diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index e8212778fa..26f91556d6 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -30,7 +30,11 @@ jobs: - run: npm run typecheck if: '!cancelled()' - run: npm run generate-docs - - run: docker run --rm -v ${PWD}:/docs squidfunk/mkdocs-material build + - name: Build docs using docker and mkdocs-material + run: | + rm docs/README.md + docker run --rm -v ${PWD}:/docs squidfunk/mkdocs-material build --strict + unit-tests: name: Unit tests and Coverage diff --git a/.gitignore b/.gitignore index fd779aaba2..bb99e96551 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /docs/API /docs/examples /docs/example +/site/ .cache/ *.es.js *.js.map diff --git a/CHANGELOG.md b/CHANGELOG.md index 754af36e40..de839bc836 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## main ### ✨ Features and improvements +- Add sky implementation according to spec ([#3645](https://github.com/maplibre/maplibre-gl-js/pull/3645)) - _...Add new stuff here..._ ### 🐞 Bug fixes diff --git a/build/generate-doc-images.ts b/build/generate-doc-images.ts index 88b1b747c7..abf849f709 100644 --- a/build/generate-doc-images.ts +++ b/build/generate-doc-images.ts @@ -4,9 +4,10 @@ import puppeteer from 'puppeteer'; import packageJson from '../package.json' with { type: 'json' }; const exampleName = process.argv[2]; +const useLocalhost = (process.argv.length > 3) && (process.argv[3] === 'serve'); const examplePath = path.resolve('test', 'examples'); -const browser = await puppeteer.launch({headless: exampleName === 'all'}); +const browser = await puppeteer.launch({headless: true}); const page = await browser.newPage(); // set viewport and double deviceScaleFactor to get a closer shot of the map @@ -18,9 +19,13 @@ await page.setViewport({ async function createImage(exampleName) { // get the example contents - const html = fs.readFileSync(path.resolve(examplePath, `${exampleName}.html`), 'utf-8'); - - await page.setContent(html.replaceAll('../../dist', `https://unpkg.com/maplibre-gl@${packageJson.version}/dist`)); + if (useLocalhost) { + console.log('Using localhost to serve examples.'); + await page.goto(`http://localhost:9966/test/examples/${exampleName}.html`); + } else { + const html = fs.readFileSync(path.resolve(examplePath, `${exampleName}.html`), 'utf-8'); + await page.setContent(html.replaceAll('../../dist', `https://unpkg.com/maplibre-gl@${packageJson.version}/dist`)); + } // Wait for map to load, then wait two more seconds for images, etc. to load. try { @@ -61,8 +66,12 @@ if (exampleName === 'all') { } else if (exampleName) { await createImage(exampleName); } else { - throw new Error( - '\n Usage: npm run generate-images \nExample: npm run generate-images 3d-buildings' + throw new Error(` + Usage: npm run generate-images [serve] + file-name: the name of the example file in test/examples without the .html extension. + all: generate images for all examples. + serve: use localhost to serve examples - use 'npm run start' with this option, otherwise it will use the latest published version in npm. + Example: npm run generate-images 3d-buildings serve` ); } diff --git a/docs/README.md b/docs/README.md index 3f15e5f5f7..1fe4616a10 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,7 +59,7 @@ Examples are written as regular html files in `test/examples`. Each example shou When you create a new example, you **must** make an accompanying image. 1. Run `npm run generate-images `. The script will take a screenshot of the map in the example and save it to `docs/assets/examples/`. -2. Optimize the image with [Squoosh](https://squoosh.app/) to reduce the file size. (Optional) +2. Optimize the image with [compresspng](https://compresspng.com/) to reduce the file size. (Optional) 3. Commit the image. For some examples, `npm run generate-images` does not generate an ideal image. In these cases, you can interact with the map after running the command before the screenshot is taken, or take a screenshot yourself by running the site locally with `npm start`, take a screenshot and save it in the `docs/assets/examples/` folder. diff --git a/docs/assets/examples/sky-with-fog-and-terrain.png b/docs/assets/examples/sky-with-fog-and-terrain.png new file mode 100644 index 0000000000..6a74b36451 Binary files /dev/null and b/docs/assets/examples/sky-with-fog-and-terrain.png differ diff --git a/docs/guides/large-data.md b/docs/guides/large-data.md index 71a621ddfc..8815a32a97 100644 --- a/docs/guides/large-data.md +++ b/docs/guides/large-data.md @@ -63,7 +63,7 @@ Once the data is loaded, to ensure a smooth user experience, it's essential to o One simple approach is to visualise fewer points. If we are using a GeoJSON source (i.e. not vector tiles), we can use 'clustering' to group nearby points together. This approach reduces the number of features displayed on the map, improving rendering performance and maintaining map readability. -To do this, when we add the data, we can adjust the [cluster options](/maplibre-gl-js/docs/API/type-aliases/SetClusterOptions/). For example: +To do this, when we add the data, we can adjust the [cluster options](../API/type-aliases/SetClusterOptions.md). For example: ```javascript map.addSource('earthquakes', { diff --git a/mkdocs.yml b/mkdocs.yml index 68af756a4d..8193ad7994 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -67,3 +67,8 @@ plugins: - social: cards_layout_options: background_color: '#295DAA' +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + anchors: warn diff --git a/src/geo/transform.ts b/src/geo/transform.ts index 388a0189d7..39eb5f5eb7 100644 --- a/src/geo/transform.ts +++ b/src/geo/transform.ts @@ -36,6 +36,7 @@ export class Transform { modelViewProjectionMatrix: mat4; invModelViewProjectionMatrix: mat4; alignedModelViewProjectionMatrix: mat4; + fogMatrix: mat4; pixelMatrix: mat4; pixelMatrix3D: mat4; pixelMatrixInverse: mat4; @@ -58,6 +59,7 @@ export 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 @@ -83,6 +85,7 @@ export class Transform { this._edgeInsets = new EdgeInsets(); this._posMatrixCache = {}; this._alignedPosMatrixCache = {}; + this._fogMatrixCache = {}; this.minElevationForCurrentTile = 0; } @@ -690,6 +693,17 @@ export 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 - the tile ID @@ -701,19 +715,32 @@ export 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.alignedModelViewProjectionMatrix : this.modelViewProjectionMatrix, 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 - the tile ID + * @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; } @@ -910,6 +937,20 @@ export class Transform { this.modelViewProjectionMatrix = m; this.invModelViewProjectionMatrix = 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, 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 world space to screen coordinates in 3D this.pixelMatrix3D = mat4.multiply(new Float64Array(16) as any, this.labelPlaneMatrix, m); @@ -934,6 +975,7 @@ export 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..0ccfa8fbd9 --- /dev/null +++ b/src/render/draw_sky.ts @@ -0,0 +1,44 @@ +import {StencilMode} from '../gl/stencil_mode'; +import {DepthMode} from '../gl/depth_mode'; +import {CullFaceMode} from '../gl/cull_face_mode'; +import {PosArray, TriangleIndexArray} from '../data/array_types.g'; +import posAttributes from '../data/pos_attributes'; +import {SegmentVector} from '../data/segment'; +import {skyUniformValues} from './program/sky_program'; +import {Sky} from '../style/sky'; +import {Mesh} from './mesh'; +import type {Painter} from './painter'; + +export 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'); + + if (!sky.mesh) { + 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); + + sky.mesh = new Mesh( + context.createVertexBuffer(vertexArray, posAttributes.members), + context.createIndexBuffer(indexArray), + SegmentVector.simpleSegment(0, 0, vertexArray.length, indexArray.length) + ); + } + + program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, + CullFaceMode.disabled, skyUniforms, undefined, 'sky', sky.mesh.vertexBuffer, + sky.mesh.indexBuffer, sky.mesh.segments); +} diff --git a/src/render/draw_terrain.ts b/src/render/draw_terrain.ts index 688c578146..aa3cfc2221 100644 --- a/src/render/draw_terrain.ts +++ b/src/render/draw_terrain.ts @@ -84,7 +84,9 @@ function drawTerrain(painter: Painter, terrain: Terrain, tiles: Array) { context.activeTexture.set(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture.texture); const posMatrix = painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped()); - const uniformValues = terrainUniformValues(posMatrix, terrain.getMeshFrameDelta(painter.transform.zoom)); + const eleDelta = terrain.getMeshFrameDelta(painter.transform.zoom); + const fogMatrix = painter.transform.calculateFogMatrix(tile.tileID.toUnwrapped()); + 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); } diff --git a/src/render/mesh.ts b/src/render/mesh.ts new file mode 100644 index 0000000000..0d2986ee45 --- /dev/null +++ b/src/render/mesh.ts @@ -0,0 +1,25 @@ +import {SegmentVector} from '../data/segment'; +import {VertexBuffer} from '../gl/vertex_buffer'; +import {IndexBuffer} from '../gl/index_buffer'; + +export class Mesh { + vertexBuffer: VertexBuffer; + indexBuffer: IndexBuffer; + segments: SegmentVector; + + constructor(vertexBuffer: VertexBuffer, indexBuffer: IndexBuffer, segments: SegmentVector) { + this.vertexBuffer = vertexBuffer; + this.indexBuffer = indexBuffer; + this.segments = segments; + } + + destroy(): void { + this.vertexBuffer.destroy(); + this.indexBuffer.destroy(); + this.segments.destroy(); + + this.vertexBuffer = null; + this.indexBuffer = null; + this.segments = null; + } +} diff --git a/src/render/painter.ts b/src/render/painter.ts index 55c28328e9..abb4d06636 100644 --- a/src/render/painter.ts +++ b/src/render/painter.ts @@ -33,6 +33,8 @@ import {drawDebug, drawDebugPadding, selectDebugSource} from './draw_debug'; import {drawCustom} from './draw_custom'; import {drawDepth, drawCoords} from './draw_terrain'; import {OverscaledTileID} from '../source/tile_id'; +import {RenderToTexture} from './render_to_texture'; +import {drawSky} from './draw_sky'; import type {Transform} from '../geo/transform'; import type {Tile} from '../source/tile'; @@ -46,7 +48,6 @@ import type {VertexBuffer} from '../gl/vertex_buffer'; import type {IndexBuffer} from '../gl/index_buffer'; import type {DepthRangeType, DepthMaskType, DepthFuncType} from '../gl/types'; import type {ResolvedImage} from '@maplibre/maplibre-gl-style-spec'; -import {RenderToTexture} from './render_to_texture'; export type RenderPass = 'offscreen' | 'opaque' | 'translucent'; @@ -407,6 +408,9 @@ export 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.stylesheet.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..18457ca5b5 --- /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_horizon_color': UniformColor; + 'u_horizon': Uniform1f; + 'u_sky_horizon_blend': Uniform1f; +}; + +const skyUniforms = (context: Context, locations: UniformLocations): SkyUniformsType => ({ + 'u_sky_color': new UniformColor(context, locations.u_sky_color), + 'u_horizon_color': new UniformColor(context, locations.u_horizon_color), + 'u_horizon': new Uniform1f(context, locations.u_horizon), + 'u_sky_horizon_blend': new Uniform1f(context, locations.u_sky_horizon_blend), +}); + +const skyUniformValues = (sky: Sky, transform: Transform, pixelRatio: number): UniformValues => ({ + 'u_sky_color': sky.properties.get('sky-color'), + 'u_horizon_color': sky.properties.get('horizon-color'), + 'u_horizon': (transform.height / 2 + transform.getHorizon()) * pixelRatio, + 'u_sky_horizon_blend': (sky.properties.get('sky-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 a0c13ef4cc..f6275e0927 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 '@maplibre/maplibre-gl-style-spec'; export type TerrainPreludeUniformsType = { 'u_depth': Uniform1i; @@ -21,6 +24,12 @@ export type TerrainUniformsType = { 'u_matrix': UniformMatrix4f; 'u_texture': Uniform1i; 'u_ele_delta': Uniform1f; + 'u_fog_matrix': UniformMatrix4f; + 'u_fog_color': UniformColor; + 'u_fog_ground_blend': Uniform1f; + 'u_fog_ground_blend_opacity': Uniform1f; + 'u_horizon_color': UniformColor; + 'u_horizon_fog_blend': Uniform1f; }; export type TerrainDepthUniformsType = { @@ -47,7 +56,13 @@ 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_ground_blend': new Uniform1f(context, locations.u_fog_ground_blend), + 'u_fog_ground_blend_opacity': new Uniform1f(context, locations.u_fog_ground_blend_opacity), + 'u_horizon_color': new UniformColor(context, locations.u_horizon_color), + 'u_horizon_fog_blend': new Uniform1f(context, locations.u_horizon_fog_blend) }); const terrainDepthUniforms = (context: Context, locations: UniformLocations): TerrainDepthUniformsType => ({ @@ -64,11 +79,19 @@ const terrainCoordsUniforms = (context: Context, locations: UniformLocations): T const terrainUniformValues = ( matrix: mat4, - eleDelta: number -): UniformValues => ({ + 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_ground_blend': sky ? sky.properties.get('fog-ground-blend') : 1, + 'u_fog_ground_blend_opacity': sky ? sky.calculateFogBlendOpacity(pitch) : 0, + 'u_horizon_color': sky ? sky.properties.get('horizon-color') : Color.white, + 'u_horizon_fog_blend': sky ? sky.properties.get('horizon-fog-blend') : 1 }); const terrainDepthUniformValues = ( diff --git a/src/render/render_to_texture.test.ts b/src/render/render_to_texture.test.ts index 6b8ae3fde4..40da47e7db 100644 --- a/src/render/render_to_texture.test.ts +++ b/src/render/render_to_texture.test.ts @@ -64,7 +64,7 @@ describe('render to texture', () => { const painter = { layersDrawn: 0, context: new Context(gl), - transform: {zoom: 10, calculatePosMatrix: () => {}}, + transform: {zoom: 10, calculatePosMatrix: () => {}, calculateFogMatrix: () => {}}, colorModeForRenderPass: () => ColorMode.alphaBlended, useProgram: () => { return {draw: () => { layersDrawn++; }}; }, _renderTileClippingMasks: () => {}, diff --git a/src/render/terrain.ts b/src/render/terrain.ts index d523d2feb7..6dd87013aa 100644 --- a/src/render/terrain.ts +++ b/src/render/terrain.ts @@ -7,8 +7,6 @@ import {warnOnce} from '../util/util'; import {Pos3dArray, TriangleIndexArray} from '../data/array_types.g'; import pos3dAttributes from '../data/pos3d_attributes'; import {SegmentVector} from '../data/segment'; -import {VertexBuffer} from '../gl/vertex_buffer'; -import {IndexBuffer} from '../gl/index_buffer'; import {Painter} from './painter'; import {Texture} from '../render/texture'; import type {Framebuffer} from '../gl/framebuffer'; @@ -19,6 +17,7 @@ import {SourceCache} from '../source/source_cache'; import {EXTENT} from '../data/extent'; import type {TerrainSpecification} from '@maplibre/maplibre-gl-style-spec'; import {LngLat, earthRadius} from '../geo/lng_lat'; +import {Mesh} from './mesh'; /** * @internal @@ -36,16 +35,6 @@ export type TerrainData = { tile: Tile; } -/** - * @internal - * A terrain mesh object - */ -export type TerrainMesh = { - indexBuffer: IndexBuffer; - vertexBuffer: VertexBuffer; - segments: SegmentVector; -} - /** * @internal * This is the main class which handles most of the 3D Terrain logic. It has the following topics: @@ -118,7 +107,7 @@ export class Terrain { * GL Objects for the terrain-mesh * The mesh is a regular mesh, which has the advantage that it can be reused for all tiles. */ - _mesh: TerrainMesh; + _mesh: Mesh; /** * coords index contains a list of tileID.keys. This index is used to identify * the tile via the alpha-cannel in the coords-texture. @@ -381,7 +370,7 @@ export class Terrain { * create a regular mesh which will be used by all terrain-tiles * @returns the created regular mesh */ - getTerrainMesh(): TerrainMesh { + getTerrainMesh(): Mesh { if (this._mesh) return this._mesh; const context = this.painter.context; const vertexArray = new Pos3dArray(); @@ -415,11 +404,11 @@ export class Terrain { indexArray.emplaceBack(offsetRight + y, offsetRight + y + 3, offsetRight + y + 1); indexArray.emplaceBack(offsetRight + y, offsetRight + y + 2, offsetRight + y + 3); } - this._mesh = { - indexBuffer: context.createIndexBuffer(indexArray), - vertexBuffer: context.createVertexBuffer(vertexArray, pos3dAttributes.members), - segments: SegmentVector.simpleSegment(0, 0, vertexArray.length, indexArray.length) - }; + this._mesh = new Mesh( + context.createVertexBuffer(vertexArray, pos3dAttributes.members), + context.createIndexBuffer(indexArray), + SegmentVector.simpleSegment(0, 0, vertexArray.length, indexArray.length) + ); return this._mesh; } diff --git a/src/shaders/shaders.ts b/src/shaders/shaders.ts index 383df3c9f7..12711ad4e3 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'; export const shaders = { prelude: compile(preludeFrag, preludeVert), @@ -86,9 +90,9 @@ export 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)}; // Expand #pragmas to #ifdefs. diff --git a/src/shaders/sky.fragment.glsl b/src/shaders/sky.fragment.glsl new file mode 100644 index 0000000000..e65b97c030 --- /dev/null +++ b/src/shaders/sky.fragment.glsl @@ -0,0 +1,17 @@ +uniform vec4 u_sky_color; +uniform vec4 u_horizon_color; + +uniform float u_horizon; +uniform float u_sky_horizon_blend; + +void main() { + float y = gl_FragCoord.y; + if (y > u_horizon) { + float blend = y - u_horizon; + if (blend < u_sky_horizon_blend) { + gl_FragColor = mix(u_sky_color, u_horizon_color, pow(1.0 - blend / u_sky_horizon_blend, 2.0)); + } else { + gl_FragColor = u_sky_color; + } + } +} \ No newline at end of file diff --git a/src/shaders/sky.vertex.glsl b/src/shaders/sky.vertex.glsl new file mode 100644 index 0000000000..3425b29a21 --- /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); +} \ No newline at end of file diff --git a/src/shaders/terrain.fragment.glsl b/src/shaders/terrain.fragment.glsl index de44a497e1..56b1fcb8f6 100644 --- a/src/shaders/terrain.fragment.glsl +++ b/src/shaders/terrain.fragment.glsl @@ -1,7 +1,32 @@ uniform sampler2D u_texture; +uniform vec4 u_fog_color; +uniform vec4 u_horizon_color; +uniform float u_fog_ground_blend; +uniform float u_fog_ground_blend_opacity; +uniform float u_horizon_fog_blend; in vec2 v_texture_pos; +in float v_fog_depth; + +const float gamma = 2.2; + +vec4 gammaToLinear(vec4 color) { + return pow(color, vec4(gamma)); +} + +vec4 linearToGamma(vec4 color) { + return pow(color, vec4(1.0 / gamma)); +} void main() { - fragColor = texture(u_texture, v_texture_pos); + vec4 surface_color = texture2D(u_texture, v_texture_pos); + if (v_fog_depth > u_fog_ground_blend) { + vec4 surface_color_linear = gammaToLinear(surface_color); + float blend_color = smoothstep(0.0, 1.0, max((v_fog_depth - u_horizon_fog_blend) / (1.0 - u_horizon_fog_blend), 0.0)); + vec4 fog_horizon_color_linear = mix(gammaToLinear(u_fog_color), gammaToLinear(u_horizon_color), blend_color); + float factor_fog = max(v_fog_depth - u_fog_ground_blend, 0.0) / (1.0 - u_fog_ground_blend); + gl_FragColor = linearToGamma(mix(surface_color_linear, fog_horizon_color_linear, pow(factor_fog, 2.0) * u_fog_ground_blend_opacity)); + } else { + gl_FragColor = surface_color; + } } diff --git a/src/shaders/terrain.vertex.glsl b/src/shaders/terrain.vertex.glsl index 6e09194480..2a80424204 100644 --- a/src/shaders/terrain.vertex.glsl +++ b/src/shaders/terrain.vertex.glsl @@ -1,15 +1,17 @@ in vec3 a_pos3d; uniform mat4 u_matrix; +uniform mat4 u_fog_matrix; uniform float u_ele_delta; out vec2 v_texture_pos; -out float v_depth; +out 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; +} \ No newline at end of file diff --git a/src/shaders/terrain_coords.vertex.glsl b/src/shaders/terrain_coords.vertex.glsl new file mode 100644 index 0000000000..08d29ba981 --- /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; + +out 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); +} \ No newline at end of file diff --git a/src/shaders/terrain_depth.vertex.glsl b/src/shaders/terrain_depth.vertex.glsl new file mode 100644 index 0000000000..9c080b4c22 --- /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; + +out 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; +} \ No newline at end of file diff --git a/src/style/light.ts b/src/style/light.ts index 348045f6f2..a99e329b4b 100644 --- a/src/style/light.ts +++ b/src/style/light.ts @@ -48,14 +48,14 @@ class LightPositionProperty implements Property<[number, number, number], LightP } } -type Props = { +type LightProps = { 'anchor': DataConstantProperty<'map' | 'viewport'>; 'position': LightPositionProperty; 'color': DataConstantProperty; 'intensity': DataConstantProperty; }; -type PropsPossiblyEvaluated = { +type LightPropsPossiblyEvaluated = { 'anchor': 'map' | 'viewport'; 'position': LightPosition; 'color': Color; @@ -64,15 +64,15 @@ type PropsPossiblyEvaluated = { const TRANSITION_SUFFIX = '-transition'; -let lightProperties: Properties; +let lightProperties: Properties; /* * Represents the light used to light extruded features. */ export class Light extends Evented { - _transitionable: Transitionable; - _transitioning: Transitioning; - properties: PossiblyEvaluated; + _transitionable: Transitionable; + _transitioning: Transitioning; + properties: PossiblyEvaluated; constructor(lightOptions?: LightSpecification) { super(); @@ -99,9 +99,9 @@ export class Light extends Evented { for (const name in light) { const value = light[name]; if (name.endsWith(TRANSITION_SUFFIX)) { - this._transitionable.setTransition(name.slice(0, -TRANSITION_SUFFIX.length) as keyof Props, value); + this._transitionable.setTransition(name.slice(0, -TRANSITION_SUFFIX.length) as keyof LightProps, value); } else { - this._transitionable.setValue(name as keyof Props, value); + this._transitionable.setValue(name as keyof LightProps, value); } } } diff --git a/src/style/sky.ts b/src/style/sky.ts new file mode 100644 index 0000000000..261407d9fc --- /dev/null +++ b/src/style/sky.ts @@ -0,0 +1,116 @@ +import {DataConstantProperty, PossiblyEvaluated, Properties, Transitionable, Transitioning, TransitionParameters} from './properties'; +import {Evented} from '../util/evented'; +import {EvaluationParameters} from './evaluation_parameters'; +import {emitValidationErrors, validateSky, validateStyle} from './validate_style'; +import {extend} from '../util/util'; +import {Color, latest as styleSpec} from '@maplibre/maplibre-gl-style-spec'; +import {Mesh} from '../render/mesh'; +import type {StylePropertySpecification, SkySpecification} from '@maplibre/maplibre-gl-style-spec'; +import type {StyleSetterOptions} from './style'; + +type SkyProps = { + 'sky-color': DataConstantProperty; + 'horizon-color': DataConstantProperty; + 'fog-color': DataConstantProperty; + 'fog-ground-blend': DataConstantProperty; + 'horizon-fog-blend': DataConstantProperty; + 'sky-horizon-blend': DataConstantProperty; + 'atmosphere-blend': DataConstantProperty; +}; + +type SkyPropsPossiblyEvaluated = { + 'sky-color': Color; + 'horizon-color': Color; + 'fog-color': Color; + 'fog-ground-blend': number; + 'horizon-fog-blend': number; + 'sky-horizon-blend': number; + 'atmosphere-blend': number; +}; + +const properties: Properties = new Properties({ + 'sky-color': new DataConstantProperty(styleSpec.sky['sky-color'] as StylePropertySpecification), + 'horizon-color': new DataConstantProperty(styleSpec.sky['horizon-color'] as StylePropertySpecification), + 'fog-color': new DataConstantProperty(styleSpec.sky['fog-color'] as StylePropertySpecification), + 'fog-ground-blend': new DataConstantProperty(styleSpec.sky['fog-ground-blend'] as StylePropertySpecification), + 'horizon-fog-blend': new DataConstantProperty(styleSpec.sky['horizon-fog-blend'] as StylePropertySpecification), + 'sky-horizon-blend': new DataConstantProperty(styleSpec.sky['sky-horizon-blend'] as StylePropertySpecification), + 'atmosphere-blend': new DataConstantProperty(styleSpec.sky['atmosphere-blend'] as StylePropertySpecification) +}); + +const TRANSITION_SUFFIX = '-transition'; + +export class Sky extends Evented { + properties: PossiblyEvaluated; + + /** + * This is used to cache the gl mesh for the sky, it should be initialized only once. + */ + mesh: Mesh | undefined; + _transitionable: Transitionable; + _transitioning: Transitioning; + + constructor(sky?: SkySpecification) { + super(); + this._transitionable = new Transitionable(properties); + this.setSky(sky); + this._transitioning = this._transitionable.untransitioned(); + } + + setSky(sky?: SkySpecification, options: StyleSetterOptions = {}) { + if (this._validate(validateSky, sky, options)) 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 SkyProps, value); + } else { + this._transitionable.setValue(name as keyof SkyProps, value); + } + } + } + + getSky(): SkySpecification { + 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, options: StyleSetterOptions = {}) { + if (options?.validate === false) { + return false; + } + 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: number) { + if (pitch < 60) return 0; // disable + if (pitch < 70) return (pitch - 60) / 10; // fade in + return 1; + } +} diff --git a/src/style/style.test.ts b/src/style/style.test.ts index e4cb370604..fc10b7b5d5 100644 --- a/src/style/style.test.ts +++ b/src/style/style.test.ts @@ -12,7 +12,7 @@ import {OverscaledTileID} from '../source/tile_id'; import {fakeServer, type FakeServer} from 'nise'; import {EvaluationParameters} from './evaluation_parameters'; -import {LayerSpecification, GeoJSONSourceSpecification, FilterSpecification, SourceSpecification, StyleSpecification, SymbolLayerSpecification, TerrainSpecification} from '@maplibre/maplibre-gl-style-spec'; +import {LayerSpecification, GeoJSONSourceSpecification, FilterSpecification, SourceSpecification, StyleSpecification, SymbolLayerSpecification, TerrainSpecification, SkySpecification} from '@maplibre/maplibre-gl-style-spec'; import {GeoJSONSource} from '../source/geojson_source'; import {sleep} from '../util/test/util'; import {RTLPluginLoadedEventName} from '../source/rtl_text_plugin_status'; @@ -695,6 +695,7 @@ describe('Style#setState', () => { spys.push(jest.spyOn(style, 'setGeoJSONSourceData').mockImplementation((() => {}) as any)); spys.push(jest.spyOn(style, 'setGlyphs').mockImplementation((() => {}) as any)); spys.push(jest.spyOn(style, 'setSprite').mockImplementation((() => {}) as any)); + spys.push(jest.spyOn(style, 'setSky').mockImplementation((() => {}) as any)); spys.push(jest.spyOn(style.map, 'setTerrain').mockImplementation((() => {}) as any)); const newStyle = JSON.parse(JSON.stringify(styleJson)) as StyleSpecification; @@ -723,6 +724,11 @@ describe('Style#setState', () => { exaggeration: 0.5 }; newStyle.zoom = 2; + newStyle.sky = { + 'fog-color': '#000001', + 'sky-color': '#000002', + 'horizon-fog-blend': 0.5, + }; const didChange = style.setState(newStyle); expect(didChange).toBeTruthy(); for (const spy of spys) { @@ -2530,4 +2536,75 @@ describe('Style#serialize', () => { await style.once('style.load'); expect(style.serialize().terrain).toBeUndefined(); }); + + test('include sky property when map has sky', async () => { + const sky: SkySpecification = { + 'horizon-fog-blend': 0.5, + 'fog-color': '#fff' + }; + const styleJson = createStyleJSON({sky}); + const style = new Style(getStubMap()); + style.loadJSON(styleJson); + + await style.once('style.load'); + expect(style.serialize().sky).toStrictEqual(sky); + }); + + test('do not include sky property when map does not have sky', async () => { + const style = new Style(getStubMap()); + style.loadJSON(createStyleJSON()); + + await style.once('style.load'); + expect(style.serialize().sky).toBeUndefined(); + }); + + test('sky should be undefined when map does not have sky', async () => { + const style = new Style(getStubMap()); + style.loadJSON(createStyleJSON()); + + await style.once('style.load'); + expect(style.getSky()).toBeUndefined(); + }); + + test('do not include sky property after removing sky from the map', async () => { + const sky: SkySpecification = { + 'horizon-fog-blend': 0.5, + 'fog-color': '#fff' + }; + const styleJson = createStyleJSON({sky}); + const style = new Style(getStubMap()); + style.loadJSON(styleJson); + + await style.once('style.load'); + style.setSky(undefined); + expect(style.serialize().sky).toBeUndefined(); + }); + + test('include sky property when setting it after map loads', async () => { + const style = new Style(getStubMap()); + style.loadJSON(createStyleJSON()); + + await style.once('style.load'); + style.setSky({ + 'horizon-fog-blend': 0.5, + 'fog-color': '#fff' + }); + expect(style.serialize().sky).toBeDefined(); + }); + + test('update sky properties after setting the sky on initial load', async () => { + const sky: SkySpecification = { + 'fog-color': '#FF0000' + }; + const style = new Style(getStubMap()); + style.loadJSON(createStyleJSON({sky, transition: {duration: 0, delay: 0}})); + + await style.once('style.load'); + style.setSky({ + 'fog-color': '#00FF00' + }); + style.update({transition: {duration: 0, delay: 0}} as EvaluationParameters); + expect(style.sky.properties.get('fog-color').g).toBe(1); + expect(style.sky.properties.get('fog-color').r).toBe(0); + }); }); diff --git a/src/style/style.ts b/src/style/style.ts index 2fe3aaf36c..85c463feb6 100644 --- a/src/style/style.ts +++ b/src/style/style.ts @@ -5,6 +5,7 @@ import {loadSprite} from './load_sprite'; import {ImageManager} from '../render/image_manager'; import {GlyphManager} from '../render/glyph_manager'; import {Light} from './light'; +import {Sky} from './sky'; import {LineAtlas} from '../render/line_atlas'; import {clone, extend, deepEqual, filterObject, mapObject} from '../util/util'; import {coerceSpriteToArray} from '../util/style'; @@ -48,7 +49,8 @@ import type { LightSpecification, SourceSpecification, SpriteSpecification, - DiffOperations + DiffOperations, + SkySpecification } from '@maplibre/maplibre-gl-style-spec'; import type {CustomLayerInterface} from './style_layer/custom_style_layer'; import type {Validator} from './validate_style'; @@ -184,6 +186,7 @@ export class Style extends Evented { glyphManager: GlyphManager; lineAtlas: LineAtlas; light: Light; + sky: Sky; _frameRequest: AbortController; _loadStyleRequest: AbortController; @@ -337,6 +340,7 @@ export class Style extends Evented { this._createLayers(); this.light = new Light(this.stylesheet.light); + this.sky = new Sky(this.stylesheet.sky); this.map.setTerrain(this.stylesheet.terrain ?? null); @@ -522,6 +526,10 @@ export 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; @@ -580,6 +588,7 @@ export class Style extends Evented { } this.light.updateTransitions(parameters); + this.sky.updateTransitions(parameters); this._resetUpdates(); } @@ -624,6 +633,7 @@ export class Style extends Evented { } this.light.recalculate(parameters); + this.sky.recalculate(parameters); this.z = parameters.zoom; if (changed) { @@ -762,6 +772,9 @@ export class Style extends Evented { case 'setSprite': operations.push(() => this.setSprite.apply(this, op.args)); break; + case 'setSky': + operations.push(() => this.setSky.apply(this, op.args)); + break; case 'setTerrain': operations.push(() => this.map.setTerrain.apply(this, op.args)); break; @@ -1260,7 +1273,7 @@ export class Style extends Evented { return extend({duration: 300, delay: 0}, this.stylesheet && this.stylesheet.transition); } - serialize(): StyleSpecification { + serialize(): StyleSpecification | undefined { // We return undefined before we're loaded, following the pattern of Map.getStyle() before // the Style object is initialized. // Internally, Style._validate() calls Style.serialize() but callers are responsible for @@ -1277,6 +1290,7 @@ export class Style extends Evented { name: myStyleSheet.name, metadata: myStyleSheet.metadata, light: myStyleSheet.light, + sky: myStyleSheet.sky, center: myStyleSheet.center, zoom: myStyleSheet.zoom, bearing: myStyleSheet.bearing, @@ -1474,6 +1488,39 @@ export class Style extends Evented { this.light.updateTransitions(parameters); } + getSky(): SkySpecification { + return this.stylesheet?.sky; + } + + setSky(skyOptions?: SkySpecification, options: StyleSetterOptions = {}) { + const sky = this.sky.getSky(); + let update = false; + if (!skyOptions) { + if (sky) { + update = true; + } + } + for (const key in skyOptions) { + if (!deepEqual(skyOptions[key], sky[key])) { + update = true; + break; + } + } + if (!update) return; + + const parameters = { + now: browser.now(), + transition: extend({ + duration: 300, + delay: 0 + }, this.stylesheet.transition) + }; + + this.stylesheet.sky = skyOptions; + this.sky.setSky(skyOptions, options); + this.sky.updateTransitions(parameters); + } + _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 9af298694f..3f8c00566c 100644 --- a/src/style/validate_style.ts +++ b/src/style/validate_style.ts @@ -17,6 +17,7 @@ type ValidateStyle = { glyphs: Validator; layer: Validator; light: Validator; + sky: Validator; terrain: Validator; filter: Validator; paintProperty: Validator; @@ -28,6 +29,7 @@ export const validateStyle = (validateStyleMin as unknown 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 9843440db9..463dd46e13 100644 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -53,7 +53,8 @@ import type { StyleSpecification, LightSpecification, SourceSpecification, - TerrainSpecification + TerrainSpecification, + SkySpecification } from '@maplibre/maplibre-gl-style-spec'; import type {MapGeoJSONFeature} from '../util/vectortile_to_geojson'; import type {ControlPosition, IControl} from './control/control'; @@ -2661,6 +2662,31 @@ export class Map extends Camera { return this.style.getLight(); } + /** + * Loads sky and fog defined by {@link SkySpecification} onto the map. + * Note: The fog only shows when using the terrain 3D feature. + * @param sky - Sky properties to set. Must conform to the [MapLibre Style Specification](https://maplibre.org/maplibre-gl-js-docs/style-spec/#sky). + * @returns `this` + * @example + * ```ts + * 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 sky Sky properties of the style. + */ + getSky() { + return this.style.getSky(); + } + /** * Sets the `state` of a feature. * A feature's `state` is a set of user-defined key-value pairs that are assigned to a feature at runtime. diff --git a/src/ui/map_tests/map_style.test.ts b/src/ui/map_tests/map_style.test.ts index 5279a20b85..ce7f50a24c 100644 --- a/src/ui/map_tests/map_style.test.ts +++ b/src/ui/map_tests/map_style.test.ts @@ -475,4 +475,44 @@ describe('#getStyle', () => { expect(spy).not.toHaveBeenCalled(); }); + describe('#setSky', () => { + test('calls style setSky when set', () => { + const map = createMap(); + const spy = jest.fn(); + map.style.setSky = spy; + map.setSky({'horizon-fog-blend': 0.5}); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('#getSky', () => { + test('returns undefined when not set', () => { + const map = createMap(); + expect(map.getSky()).toBeUndefined(); + }); + }); + + describe('#setLight', () => { + test('calls style setLight when set', () => { + const map = createMap(); + const spy = jest.fn(); + map.style.setLight = spy; + map.setLight({anchor: 'viewport'}); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('#getLight', () => { + test('calls style getLight when invoked', () => { + const map = createMap(); + const spy = jest.fn(); + map.style.getLight = spy; + map.getLight(); + + expect(spy).toHaveBeenCalled(); + }); + }); + }); diff --git a/test/build/min.test.ts b/test/build/min.test.ts index db21651810..8cc9002488 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -36,7 +36,7 @@ describe('test min build', () => { const decreaseQuota = 4096; // feel free to update this value after you've checked that it has changed on purpose :-) - const expectedBytes = 793823; + const expectedBytes = 799999; expect(actualBytes - expectedBytes).toBeLessThan(increaseQuota); expect(expectedBytes - actualBytes).toBeLessThan(decreaseQuota); diff --git a/test/examples/3d-terrain.html b/test/examples/3d-terrain.html index 2028988521..8d8000d79d 100644 --- a/test/examples/3d-terrain.html +++ b/test/examples/3d-terrain.html @@ -19,7 +19,7 @@ container: 'map', zoom: 12, center: [11.39085, 47.27574], - pitch: 52, + pitch: 70, hash: true, style: { version: 8, @@ -60,7 +60,8 @@ terrain: { source: 'terrainSource', exaggeration: 1 - } + }, + sky: {} }, maxZoom: 18, maxPitch: 85 diff --git a/test/examples/sky-with-fog-and-terrain.html b/test/examples/sky-with-fog-and-terrain.html new file mode 100644 index 0000000000..9b47f27347 --- /dev/null +++ b/test/examples/sky-with-fog-and-terrain.html @@ -0,0 +1,122 @@ + + + + Sky, Fog, Terrain + + + + + + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/test/integration/assets/tiles/zero-elevation-terrain-tile.png b/test/integration/assets/tiles/zero-elevation-terrain-tile.png new file mode 100644 index 0000000000..a445db2d8a Binary files /dev/null and b/test/integration/assets/tiles/zero-elevation-terrain-tile.png differ diff --git a/test/integration/render/tests/sky/basic/expected.png b/test/integration/render/tests/sky/basic/expected.png new file mode 100644 index 0000000000..72587cb408 Binary files /dev/null and b/test/integration/render/tests/sky/basic/expected.png differ diff --git a/test/integration/render/tests/sky/basic/style.json b/test/integration/render/tests/sky/basic/style.json new file mode 100644 index 0000000000..e2e8c9ff8b --- /dev/null +++ b/test/integration/render/tests/sky/basic/style.json @@ -0,0 +1,38 @@ +{ + "version": 8, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "timeout": 60000, + "metadata": { + "test": { + "height": 512, + "width": 512, + "maxPitch": 85, + "operations": [ + ["wait"] + ] + } + }, + "center": [ + 35.38, + 31.55 + ], + "zoom": 15, + "pitch": 85, + "sources": { }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#ccaa83" + } + } + ], + "sky": { + "sky-color": "#199EF3", + "horizon-color": "#daeff0", + "sky-horizon-blend": 1, + "fog-ground-blend": 1 + } +} diff --git a/test/integration/render/tests/terrain/fog-sky-blend/expected.png b/test/integration/render/tests/terrain/fog-sky-blend/expected.png new file mode 100644 index 0000000000..a9b443b46d Binary files /dev/null and b/test/integration/render/tests/terrain/fog-sky-blend/expected.png differ diff --git a/test/integration/render/tests/terrain/fog-sky-blend/style.json b/test/integration/render/tests/terrain/fog-sky-blend/style.json new file mode 100644 index 0000000000..3e6ce98039 --- /dev/null +++ b/test/integration/render/tests/terrain/fog-sky-blend/style.json @@ -0,0 +1,62 @@ +{ + "version": 8, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "timeout": 60000, + "metadata": { + "test": { + "height": 512, + "width": 512, + "maxPitch": 85, + "operations": [ + ["wait"] + ] + } + }, + "center": [ + 35.38, + 31.55 + ], + "zoom": 15, + "pitch": 85, + "sources": { + "hillshadeSource": { + "type": "raster-dem", + "tiles": [ + "local://tiles/zero-elevation-terrain-tile.png" + ], + "minzoom": 0, + "maxzoom": 12 + }, + "terrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/zero-elevation-terrain-tile.png" + ], + "minzoom": 7, + "maxzoom": 12, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ], + "terrain": { + "source": "terrain", + "exaggeration": 1 + }, + "sky": { + "sky-color": "#00ff00", + "horizon-color": "#00ff00", + "fog-color": "#ff0000", + "sky-horizon-blend": 1, + "horizon-fog-blend": 0, + "fog-ground-blend": 0 + } +} diff --git a/test/integration/render/tests/terrain/fog/expected.png b/test/integration/render/tests/terrain/fog/expected.png new file mode 100644 index 0000000000..045f70a4b7 Binary files /dev/null and b/test/integration/render/tests/terrain/fog/expected.png differ diff --git a/test/integration/render/tests/terrain/fog/style.json b/test/integration/render/tests/terrain/fog/style.json new file mode 100644 index 0000000000..22bcc22142 --- /dev/null +++ b/test/integration/render/tests/terrain/fog/style.json @@ -0,0 +1,91 @@ +{ + "version": 8, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "timeout": 60000, + "metadata": { + "test": { + "height": 512, + "width": 512, + "maxPitch": 85, + "operations": [ + [ + "wait" + ], + [ + "setTerrain" + ], + [ + "wait" + ], + [ + "setTerrain", + { + "source": "terrain", + "exaggeration": 1 + } + ], + [ + "wait" + ] + ] + } + }, + "center": [ + 35.38, + 31.55 + ], + "zoom": 15, + "pitch": 85, + "sources": { + "hillshadeSource": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain/{z}-{x}-{y}.terrain.png" + ], + "minzoom": 0, + "maxzoom": 12 + }, + "terrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain/{z}-{x}-{y}.terrain.png" + ], + "minzoom": 7, + "maxzoom": 12, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#ccaa83" + } + }, + { + "id": "hills", + "type": "hillshade", + "source": "hillshadeSource", + "layout": { + "visibility": "visible" + }, + "paint": { + "hillshade-shadow-color": "#473B24", + "hillshade-illumination-anchor": "map", + "hillshade-illumination-direction": 150 + } + } + ], + "terrain": { + "source": "terrain", + "exaggeration": 1 + }, + "sky": { + "sky-color": "#199EF3", + "fog-color": "#daeff0", + "sky-horizon-blend": 0, + "fog-ground-blend": 0 + } +}
sky-color
horizon-color
fog-color
sky-horizon-blend
horizon-fog-blend
fog-ground-blend