Skip to content

Commit

Permalink
Sky Implementation according to spec (#3645)
Browse files Browse the repository at this point in the history
* copy code from old sky branch, but this branch is not ready yet

* add missing _fogMatrixCache clearance

* change long comment from // to /** */ syntax

* serialize sky in map.getStyle()

* add sky/fog expect tests (#3649)

* fix sky2 calculateFogMatrix error and build error (#3651)

* try to fix calculateFogMatrix error

* Raise expectedBytes for maplibre-gl.js size

* Update test/build/min.test.ts

* Update diff function to take into consideration sky, updated tests

* Move gl logic to draw_sky file, added tests to cover serialization.

* Update docs comment

* Add missing tests to map.test.ts

* Remove unneeded member variable

* Fix build

* Add cache to draw sky mesh buffer

* Use globe's mesh idea, store mesh in sky object, similar to how it is done with buckets.

* Fix tests

* Fix names according to new spec

* Fix render and build tests

* Changed name to sky-horizon-blend

* Fix lint

* Add support for blend in fog and horizon

* Terrain fog blending improvements (#4314)

* Incorporate my own feedback

* Fix shader

* Make terrain fog shader blending gamma-correct, update render test

* Move surface_color_linear into the branch

* Added a debug page, fixed a bug

* Update the example to show the values in the spec.

* Added test to reproduce the issue

* Move sky test to a different folder

* Fix lint

* Remove unneeded reference from example

* Always initialize sky, remove ifs, fix tests

* Update CHANGELOG.md

* Expect build to fail due to missing image in examples

* Fix docs build

* Fix docs build

* Compress image

* Update readme

---------

Co-authored-by: Max Demmelbauer <[email protected]>
Co-authored-by: Andrew Calcutt <[email protected]>
Co-authored-by: Harel M <[email protected]>
Co-authored-by: Jakub Pelc <[email protected]>
  • Loading branch information
5 people authored Jun 27, 2024
1 parent 8fe33a6 commit 6612f1b
Show file tree
Hide file tree
Showing 42 changed files with 949 additions and 69 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/test-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/docs/API
/docs/examples
/docs/example
/site/
.cache/
*.es.js
*.js.map
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
21 changes: 15 additions & 6 deletions build/generate-doc-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -61,8 +66,12 @@ if (exampleName === 'all') {
} else if (exampleName) {
await createImage(exampleName);
} else {
throw new Error(
'\n Usage: npm run generate-images <file-name|all>\nExample: npm run generate-images 3d-buildings'
throw new Error(`
Usage: npm run generate-images <file-name|all> [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`
);
}

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <example-file-name>`. 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.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/guides/large-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
5 changes: 5 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,8 @@ plugins:
- social:
cards_layout_options:
background_color: '#295DAA'
validation:
omitted_files: warn
absolute_links: warn
unrecognized_links: warn
anchors: warn
56 changes: 49 additions & 7 deletions src/geo/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class Transform {
modelViewProjectionMatrix: mat4;
invModelViewProjectionMatrix: mat4;
alignedModelViewProjectionMatrix: mat4;
fogMatrix: mat4;
pixelMatrix: mat4;
pixelMatrix3D: mat4;
pixelMatrixInverse: mat4;
Expand All @@ -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
Expand All @@ -83,6 +85,7 @@ export class Transform {
this._edgeInsets = new EdgeInsets();
this._posMatrixCache = {};
this._alignedPosMatrixCache = {};
this._fogMatrixCache = {};
this.minElevationForCurrentTile = 0;
}

Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);

Expand All @@ -934,6 +975,7 @@ export class Transform {

this._posMatrixCache = {};
this._alignedPosMatrixCache = {};
this._fogMatrixCache = {};
}

maxPitchScaleFactor() {
Expand Down
44 changes: 44 additions & 0 deletions src/render/draw_sky.ts
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 3 additions & 1 deletion src/render/draw_terrain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ function drawTerrain(painter: Painter, terrain: Terrain, tiles: Array<Tile>) {
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);
}

Expand Down
25 changes: 25 additions & 0 deletions src/render/mesh.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 5 additions & 1 deletion src/render/painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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)];

Expand Down
4 changes: 3 additions & 1 deletion src/render/program/program_uniforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -40,5 +41,6 @@ export const programUniforms = {
backgroundPattern: backgroundPatternUniforms,
terrain: terrainUniforms,
terrainDepth: terrainDepthUniforms,
terrainCoords: terrainCoordsUniforms
terrainCoords: terrainCoordsUniforms,
sky: skyUniforms
};
28 changes: 28 additions & 0 deletions src/render/program/sky_program.ts
Original file line number Diff line number Diff line change
@@ -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<SkyUniformsType> => ({
'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};
Loading

0 comments on commit 6612f1b

Please sign in to comment.