Skip to content

Commit

Permalink
Add an Atmosphere layer for Globe (#3888) (#4020)
Browse files Browse the repository at this point in the history
* Port of PoC atmosphere layer.

* Fix resize for draw_atmosphere

* Add some options.

* Allow to change sun date and time

* Fix import warning

* Render atmosphere only when a Globe projection is selected

* Add some comments

* Add some comments

* Change key

* Update changelog

* Fix merge with globe branch

* Fix documentation and default background color.

* Use black clear color only when atmosphere is on

* Use atmosphere uniform for globe position, raidus in camera frame and inv projection matrix.

* Remove unused project method

* Update maplibre-gl-style-spec to 20.3.0 and use sky atmosphere parameter

* Fix globe tests and use light position as Sun position.

* Avoid type name collisions.

* Add atmosphere test for globe projection.

* Update expectedBytes for build test.

* Fix PR comments.

* Update Style test.

* Remove unused method on projection

* Add Sky Test.

* Fix style test and add sky unit test.

* Move getSunPos method

* Fix mercator updateProjection

* Remove isGlobe method and fix merge.

* Fix globe atmosphere tests with new projection style.

* Clean-up some projection and light. Fix setSky and add tests.

* Remove sky test during update.

* Clean-up
  • Loading branch information
Pheonor authored Jun 20, 2024
1 parent c343c43 commit 3c5e958
Show file tree
Hide file tree
Showing 81 changed files with 1,345 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- "Accept" headers set in Request Transformers are not overwritten ([#4210](https://github.com/maplibre/maplibre-gl-js/pull/4210))
- ⚠️ Rename projMatrix to modelViewProjectionMatrix. Also rename invProjMatrix, alignedProjMatrix accordingly ([#4215](https://github.com/maplibre/maplibre-gl-js/pull/4215))
- Publish an unminified prod build ([#4265](https://github.com/maplibre/maplibre-gl-js/pull/4265))
- Add option to display a realistic atmosphere when using a Globe projection ([#3888](https://github.com/maplibre/maplibre-gl-js/issues/3888))

### 🐞 Bug fixes

Expand Down
4 changes: 4 additions & 0 deletions build/generate-struct-arrays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import fillExtrusionAttributes from '../src/data/bucket/fill_extrusion_attribute
import {lineLayoutAttributes} from '../src/data/bucket/line_attributes';
import {lineLayoutAttributesExt} from '../src/data/bucket/line_attributes_ext';
import {patternAttributes} from '../src/data/bucket/pattern_attributes';
import {atmosphereAttributes} from '../src/data/atmosphere_attributes';
// symbol layer specific arrays
import {
symbolLayoutAttributes,
Expand Down Expand Up @@ -191,6 +192,9 @@ createStructArrayType('line_strip_index', createLayout([
{type: 'Uint16', name: 'vertices', components: 1}
]));

// atmosphere bounds array
createStructArrayType('atmosphere_bounds', atmosphereAttributes);

// paint vertex arrays

// used by SourceBinder for float properties
Expand Down
5 changes: 5 additions & 0 deletions src/data/atmosphere_attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {createLayout} from '../util/struct_array';

export const atmosphereAttributes = createLayout([
{name: 'a_pos', type: 'Float32', components: 4}
]);
60 changes: 47 additions & 13 deletions src/geo/projection/globe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,19 @@ export class GlobeProjection implements Projection {

private _globeProjectionOverride = true;

private _globeMatrix: mat4 = mat4.create();
private _globeMatrixNoCorrection: mat4 = mat4.create();
private _globeProjMatrix: mat4 = mat4.create();
private _globeProjMatrixNoCorrection: mat4 = mat4.create();

private _cameraPosition: vec3 = [0, 0, 0];

private _globePosition: vec3 = [0, 0, 0];
private _globeRadiusPixels: number = 0.0;

private _projMatrix: mat4 = mat4.create();
private _invProjMatrix: mat4 = mat4.create();

get name(): string {
return 'globe';
}
Expand Down Expand Up @@ -129,6 +137,18 @@ export class GlobeProjection implements Projection {
return granularitySettingsGlobe;
}

get worldCenterPosition(): vec3 {
return this._globePosition;
}

get worldSize(): number {
return this._globeRadiusPixels;
}

get invProjMatrix(): mat4 {
return this._invProjMatrix;
}

/**
* Returns whether globe view is allowed.
* When allowed, globe fill function as normal, displaying a 3D planet,
Expand Down Expand Up @@ -193,41 +213,55 @@ export class GlobeProjection implements Projection {
// We want zoom levels to be consistent between globe and flat views.
// This means that the pixel size of features at the map center point
// should be the same for both globe and flat view.
const globeRadiusPixels = transform.worldSize / (2.0 * Math.PI) / Math.cos(transform.center.lat * Math.PI / 180);
this._globeRadiusPixels = transform.worldSize / (2.0 * Math.PI) / Math.cos(transform.center.lat * Math.PI / 180);

mat4.perspective(this._projMatrix, transform.fov * Math.PI / 180, transform.width / transform.height, 0.5, transform.cameraToCenterDistance + this._globeRadiusPixels * 2.0); // just set the far plane far enough - we will calculate our own z in the vertex shader anyway
const invProjMatrix = mat4.create();
mat4.invert(invProjMatrix, this._projMatrix);
this._invProjMatrix = invProjMatrix;

// Construct a completely separate matrix for globe view
const globeMatrix = new Float64Array(16) as any;
const globeMatrixUncorrected = new Float64Array(16) as any;
mat4.perspective(globeMatrix, transform.fov * Math.PI / 180, transform.width / transform.height, 0.5, transform.cameraToCenterDistance + globeRadiusPixels * 2.0); // just set the far plane far enough - we will calculate our own z in the vertex shader anyway
const globeMatrix = mat4.identity(new Float64Array(16) as any);
mat4.translate(globeMatrix, globeMatrix, [0, 0, -transform.cameraToCenterDistance]);
mat4.rotateX(globeMatrix, globeMatrix, -transform.pitch * Math.PI / 180);
mat4.rotateZ(globeMatrix, globeMatrix, -transform.angle);
mat4.translate(globeMatrix, globeMatrix, [0.0, 0, -globeRadiusPixels]);
mat4.translate(globeMatrix, globeMatrix, [0.0, 0, -this._globeRadiusPixels]);
// Rotate the sphere to center it on viewed coordinates

// Keep a atan-correction-free matrix for transformations done on the CPU with accurate math
const globeMatrixUncorrected = new Float64Array(16) as any;
mat4.rotateX(globeMatrixUncorrected, globeMatrix, transform.center.lat * Math.PI / 180.0);
mat4.rotateY(globeMatrixUncorrected, globeMatrixUncorrected, -transform.center.lng * Math.PI / 180.0);
mat4.scale(globeMatrixUncorrected, globeMatrixUncorrected, [globeRadiusPixels, globeRadiusPixels, globeRadiusPixels]); // Scale the unit sphere to a sphere with diameter of 1
this._globeProjMatrixNoCorrection = globeMatrix;
mat4.scale(globeMatrixUncorrected, globeMatrixUncorrected, [this._globeRadiusPixels, this._globeRadiusPixels, this._globeRadiusPixels]); // Scale the unit sphere to a sphere with diameter of 1
mat4.copy(this._globeMatrixNoCorrection, globeMatrixUncorrected);
mat4.mul(this._globeProjMatrixNoCorrection, this._projMatrix, this._globeMatrixNoCorrection);

mat4.rotateX(globeMatrix, globeMatrix, transform.center.lat * Math.PI / 180.0 - this._errorCorrectionUsable);
mat4.rotateY(globeMatrix, globeMatrix, -transform.center.lng * Math.PI / 180.0);
mat4.scale(globeMatrix, globeMatrix, [globeRadiusPixels, globeRadiusPixels, globeRadiusPixels]); // Scale the unit sphere to a sphere with diameter of 1
mat4.scale(this._globeMatrix, globeMatrix, [this._globeRadiusPixels, this._globeRadiusPixels, this._globeRadiusPixels]); // Scale the unit sphere to a sphere with diameter of 1
mat4.mul(globeMatrix, this._projMatrix, this._globeMatrix);
this._globeProjMatrix = globeMatrix;

const invProj = mat4.create();
mat4.invert(invProj, globeMatrix);
const invGlobeProj = mat4.create();
mat4.invert(invGlobeProj, this._globeProjMatrix);

const cameraPos: vec4 = [0, 0, -1, 1];
vec4.transformMat4(cameraPos, cameraPos, invProj);
vec4.transformMat4(cameraPos, cameraPos, invGlobeProj);
this._cameraPosition = [
cameraPos[0] / cameraPos[3],
cameraPos[1] / cameraPos[3],
cameraPos[2] / cameraPos[3]
];

this._cachedClippingPlane = this._computeClippingPlane(transform, globeRadiusPixels);
const globePos: vec4 = [0, 0, 0, 1];
vec4.transformMat4(globePos, globePos, this._globeMatrix);
this._globePosition = [
globePos[0] / globePos[3],
globePos[1] / globePos[3],
globePos[2] / globePos[3]
];

this._cachedClippingPlane = this._computeClippingPlane(transform, this._globeRadiusPixels);
}

public getProjectionData(canonicalTileCoords: {x: number; y: number; z: number}, tilePosMatrix: mat4, useAtanCorrection: boolean = true): ProjectionData {
Expand Down Expand Up @@ -391,7 +425,7 @@ export class GlobeProjection implements Projection {
}

public transformLightDirection(transform: { center: LngLat }, dir: vec3): vec3 {
const sphereX = transform.center.lng * Math.PI / 180.0;
const sphereX = -transform.center.lng * Math.PI / 180.0;
const sphereY = transform.center.lat * Math.PI / 180.0;

const len = Math.cos(sphereY);
Expand Down
32 changes: 29 additions & 3 deletions src/geo/projection/mercator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ export const MercatorShaderDefine = '#define PROJECTION_MERCATOR';
export const MercatorShaderVariantKey = 'mercator';

export class MercatorProjection implements Projection {
private _cachedMesh: Mesh = null;
private _cachedMesh: Mesh | null = null;
private _cameraPosition: vec3 = [0, 0, 0];
private _worldCenterPosition: vec3 = [0, 0, 0];
private _worldSize: number = 0;
private _invProjMatrix: mat4 = mat4.create();

get name(): string {
return 'mercator';
Expand Down Expand Up @@ -64,6 +67,18 @@ export class MercatorProjection implements Projection {
return SubdivisionGranularitySetting.noSubdivision;
}

get worldCenterPosition(): vec3 {
return this._worldCenterPosition;
}

get worldSize(): number {
return this._worldSize;
}

get invProjMatrix(): mat4 {
return this._invProjMatrix;
}

public isRenderingDirty(): boolean {
// Mercator projection does no animations of its own, so rendering is never dirty from its perspective.
return false;
Expand All @@ -77,14 +92,25 @@ export class MercatorProjection implements Projection {
// Do nothing.
}

public updateProjection(t: { invModelViewProjectionMatrix: mat4 }): void {
public updateProjection(transform: TransformLike): void {
this._worldSize = transform.worldSize;
this._invProjMatrix = mat4.clone(transform.invModelViewProjectionMatrix);

const cameraPos: vec4 = [0, 0, -1, 1];
vec4.transformMat4(cameraPos, cameraPos, t.invModelViewProjectionMatrix);
vec4.transformMat4(cameraPos, cameraPos, transform.invModelViewProjectionMatrix);
this._cameraPosition = [
cameraPos[0] / cameraPos[3],
cameraPos[1] / cameraPos[3],
cameraPos[2] / cameraPos[3]
];

const worldPos: vec4 = [0, 0, 0, 1];
vec4.transformMat4(worldPos, worldPos, transform.invModelViewProjectionMatrix);
this._worldCenterPosition = [
worldPos[0] / worldPos[3],
worldPos[1] / worldPos[3],
worldPos[2] / worldPos[3]
];
}

public getProjectionData(canonicalTileCoords: {x: number; y: number; z: number}, tilePosMatrix: mat4): ProjectionData {
Expand Down
15 changes: 15 additions & 0 deletions src/geo/projection/projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ export interface Projection {
*/
get vertexShaderPreludeCode(): string;

/**
* World center in camera frame.
*/
get worldCenterPosition(): vec3;

/**
* World size in pixel.
*/
get worldSize(): number;

/**
* Inverse projection matrix from camera to clip plane.
*/
get invProjMatrix(): mat4;

/**
* @internal
* An object describing how much subdivision should be applied to rendered geometry.
Expand Down
80 changes: 80 additions & 0 deletions src/render/draw_atmosphere.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {StencilMode} from '../gl/stencil_mode';
import {DepthMode} from '../gl/depth_mode';
import {CullFaceMode} from '../gl/cull_face_mode';
import {atmosphereUniformValues} from './program/atmosphere_program';

import type {Painter} from './painter';
import {ColorMode} from '../gl/color_mode';
import Sky from '../style/sky';
import {Light} from '../style/light';
import {AtmosphereBoundsArray, TriangleIndexArray} from '../data/array_types.g';
import {atmosphereAttributes} from '../data/atmosphere_attributes';
import {Mesh} from './mesh';
import {SegmentVector} from '../data/segment';
import {Transform} from '../geo/transform';
import {mat4, vec3} from 'gl-matrix';

function getSunPos(light: Light, transform: Transform): vec3 {
const _lp = light.properties.get('position');
const lightPos = [-_lp.x, -_lp.y, -_lp.z] as vec3;

const lightMat = mat4.identity(new Float64Array(16) as any);

if (light.properties.get('anchor') === 'map') {
mat4.rotateX(lightMat, lightMat, -transform.pitch * Math.PI / 180);
mat4.rotateZ(lightMat, lightMat, -transform.angle);
mat4.rotateX(lightMat, lightMat, transform.center.lat * Math.PI / 180.0);
mat4.rotateY(lightMat, lightMat, -transform.center.lng * Math.PI / 180.0);
}

vec3.transformMat4(lightPos, lightPos, lightMat);

return lightPos;
}

export function drawAtmosphere(painter: Painter, sky: Sky, light: Light) {
const context = painter.context;
const gl = context.gl;
const program = painter.useProgram('atmosphere');
const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadOnly, [0, 1]);

const projection = painter.style.projection;
const projectionData = projection.getProjectionData(null, null);

const sunPos = getSunPos(light, painter.transform);

const atmosphereBlend = sky.getAtmosphereBlend();
if (atmosphereBlend === 0) {
// Don't draw anythink if atmosphere is fully transparent
return;
}

const globePosition = projection.worldCenterPosition;
const globeRadius = projection.worldSize;
const invProjMatrix = projection.invProjMatrix;

const uniformValues = atmosphereUniformValues(sunPos, atmosphereBlend, globePosition, globeRadius, invProjMatrix);

// Create the atmosphere mesh the first time we need it
if (!sky.atmosphereMesh) {
const vertexArray = new AtmosphereBoundsArray();
vertexArray.emplaceBack(-1, -1, 0.0, 1.0);
vertexArray.emplaceBack(+1, -1, 0.0, 1.0);
vertexArray.emplaceBack(+1, +1, 0.0, 1.0);
vertexArray.emplaceBack(-1, +1, 0.0, 1.0);

const indexArray = new TriangleIndexArray();
indexArray.emplaceBack(0, 1, 2);
indexArray.emplaceBack(0, 2, 3);

sky.atmosphereMesh = new Mesh(
context.createVertexBuffer(vertexArray, atmosphereAttributes.members),
context.createIndexBuffer(indexArray),
SegmentVector.simpleSegment(0, 0, vertexArray.length, indexArray.length)
);
}

const mesh = sky.atmosphereMesh;

program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, ColorMode.alphaBlended, CullFaceMode.disabled, uniformValues, null, projectionData, 'atmosphere', mesh.vertexBuffer, mesh.indexBuffer, mesh.segments);
}
6 changes: 6 additions & 0 deletions src/render/painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {drawFillExtrusion} from './draw_fill_extrusion';
import {drawHillshade} from './draw_hillshade';
import {drawRaster} from './draw_raster';
import {drawBackground} from './draw_background';
import {drawAtmosphere} from './draw_atmosphere';
import {drawDebug, drawDebugPadding, selectDebugSource} from './draw_debug';
import {drawCustom} from './draw_custom';
import {drawDepth, drawCoords} from './draw_terrain';
Expand Down Expand Up @@ -536,6 +537,11 @@ export class Painter {
this.renderLayer(this, sourceCache, layer, coords);
}

// Render atmosphere, only for Globe projection
if (this.style.projection.name === 'globe') {
drawAtmosphere(this, this.style.sky, this.style.light);
}

if (this.options.showTileBoundaries) {
const selectedSource = selectDebugSource(this.style, this.transform.zoom);
if (selectedSource) {
Expand Down
35 changes: 35 additions & 0 deletions src/render/program/atmosphere_program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type {Context} from '../../gl/context';
import {UniformValues, UniformLocations, Uniform1f, Uniform3f, UniformMatrix4f} from '../uniform_binding';
import {mat4, vec3} from 'gl-matrix';

export type atmosphereUniformsType = {
'u_sun_pos': Uniform3f;
'u_atmosphere_blend': Uniform1f;
'u_globe_position': Uniform3f;
'u_globe_radius': Uniform1f;
'u_inv_proj_matrix': UniformMatrix4f;
};

const atmosphereUniforms = (context: Context, locations: UniformLocations): atmosphereUniformsType => ({
'u_sun_pos': new Uniform3f(context, locations.u_sun_pos),
'u_atmosphere_blend': new Uniform1f(context, locations.u_atmosphere_blend),
'u_globe_position': new Uniform3f(context, locations.u_globe_position),
'u_globe_radius': new Uniform1f(context, locations.u_globe_radius),
'u_inv_proj_matrix': new UniformMatrix4f(context, locations.u_inv_proj_matrix),
});

const atmosphereUniformValues = (
sunPos: vec3,
atmosphereBlend: number,
globePosition: vec3,
globeRadius: number,
invProjMatrix: mat4,
): UniformValues<atmosphereUniformsType> => ({
'u_sun_pos': sunPos,
'u_atmosphere_blend': atmosphereBlend,
'u_globe_position': globePosition,
'u_globe_radius': globeRadius,
'u_inv_proj_matrix': invProjMatrix,
});

export {atmosphereUniforms, atmosphereUniformValues};
2 changes: 2 additions & 0 deletions src/render/program/program_uniforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {symbolIconUniforms, symbolSDFUniforms, symbolTextAndIconUniforms} from '
import {backgroundUniforms, backgroundPatternUniforms} from './background_program';
import {terrainUniforms, terrainDepthUniforms, terrainCoordsUniforms} from './terrain_program';
import {projectionErrorMeasurementUniforms} from './projection_error_measurement_program';
import {atmosphereUniforms} from './atmosphere_program';

const emptyUniforms = (_: any, __: any): any => {};

Expand Down Expand Up @@ -44,4 +45,5 @@ export const programUniforms = {
terrainDepth: terrainDepthUniforms,
terrainCoords: terrainCoordsUniforms,
projectionErrorMeasurement: projectionErrorMeasurementUniforms,
atmosphere: atmosphereUniforms,
};
6 changes: 3 additions & 3 deletions src/render/program/projection_program.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Uniform1f, Uniform4f, UniformLocations, UniformMatrix4f} from '../uniform_binding';
import {Uniform1f, Uniform3f, Uniform4f, UniformLocations, UniformMatrix4f} from '../uniform_binding';

Check warning on line 1 in src/render/program/projection_program.ts

View workflow job for this annotation

GitHub Actions / Code Hygiene

'Uniform3f' is defined but never used
import {Context} from '../../gl/context';
import {mat4} from 'gl-matrix';
import {mat4, vec3} from 'gl-matrix';

Check warning on line 3 in src/render/program/projection_program.ts

View workflow job for this annotation

GitHub Actions / Code Hygiene

'vec3' is defined but never used

export type ProjectionPreludeUniformsType = {
'u_projection_matrix': UniformMatrix4f;
Expand All @@ -15,7 +15,7 @@ export const projectionUniforms = (context: Context, locations: UniformLocations
'u_projection_tile_mercator_coords': new Uniform4f(context, locations.u_projection_tile_mercator_coords),
'u_projection_clipping_plane': new Uniform4f(context, locations.u_projection_clipping_plane),
'u_projection_transition': new Uniform1f(context, locations.u_projection_transition),
'u_projection_fallback_matrix': new UniformMatrix4f(context, locations.u_projection_fallback_matrix)
'u_projection_fallback_matrix': new UniformMatrix4f(context, locations.u_projection_fallback_matrix),
});

export type ProjectionData = {
Expand Down
Loading

0 comments on commit 3c5e958

Please sign in to comment.