diff --git a/build/generate-style-spec.ts b/build/generate-style-spec.ts
index be01b4a7b90..ae39d91b1dc 100644
--- a/build/generate-style-spec.ts
+++ b/build/generate-style-spec.ts
@@ -197,6 +197,8 @@ ${objectDeclaration('StyleSpecification', spec.$root)}
${objectDeclaration('LightSpecification', spec.light)}
+${objectDeclaration('TerrainSpecification', spec.terrain)}
+
${spec.source.map(key => objectDeclaration(sourceTypeName(key), spec[key])).join('\n\n')}
export type SourceSpecification =
diff --git a/debug/terrain.html b/debug/terrain.html
new file mode 100644
index 00000000000..e45ca8ba998
--- /dev/null
+++ b/debug/terrain.html
@@ -0,0 +1,93 @@
+
+
+
+
+ MapLibre GL JS debug page for terrian
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/css/maplibre-gl.css b/src/css/maplibre-gl.css
index 5e8fe3f0c90..6e3cf61d30f 100644
--- a/src/css/maplibre-gl.css
+++ b/src/css/maplibre-gl.css
@@ -308,6 +308,26 @@
}
}
+@svg-load ctrl-terrain url("svg/maplibregl-ctrl-terrain.svg") {
+ fill: #333;
+ #stroke { display: none; }
+}
+
+@svg-load ctrl-terrain-enabled url("svg/maplibregl-ctrl-terrain.svg") {
+ fill: #33b5e5;
+ #stroke { display: none; }
+}
+
+.maplibregl-ctrl button.maplibregl-ctrl-terrain .maplibregl-ctrl-icon,
+.mapboxgl-ctrl button.mapboxgl-ctrl-terrain .mapboxgl-ctrl-icon {
+ background-image: svg-inline(ctrl-terrain);
+}
+
+.maplibregl-ctrl button.maplibregl-ctrl-terrain-enabled .maplibregl-ctrl-icon,
+.mapboxgl-ctrl button.mapboxgl-ctrl-terrain-enabled .mapboxgl-ctrl-icon {
+ background-image: svg-inline(ctrl-terrain-enabled);
+}
+
@svg-load ctrl-geolocate url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #333;
#stroke { display: none; }
diff --git a/src/css/svg/maplibregl-ctrl-terrain.svg b/src/css/svg/maplibregl-ctrl-terrain.svg
new file mode 100644
index 00000000000..af8fefe7a58
--- /dev/null
+++ b/src/css/svg/maplibregl-ctrl-terrain.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/src/data/bucket/fill_extrusion_attributes.ts b/src/data/bucket/fill_extrusion_attributes.ts
index dcb728e73f8..e4e2936ce90 100644
--- a/src/data/bucket/fill_extrusion_attributes.ts
+++ b/src/data/bucket/fill_extrusion_attributes.ts
@@ -5,5 +5,9 @@ const layout = createLayout([
{name: 'a_normal_ed', components: 4, type: 'Int16'},
], 4);
+export const centroidAttributes = createLayout([
+ {name: 'a_centroid', components: 2, type: 'Int16'}
+], 4);
+
export default layout;
export const {members, size, alignment} = layout;
diff --git a/src/data/bucket/fill_extrusion_bucket.ts b/src/data/bucket/fill_extrusion_bucket.ts
index 6d47ff34684..13d81d4467a 100644
--- a/src/data/bucket/fill_extrusion_bucket.ts
+++ b/src/data/bucket/fill_extrusion_bucket.ts
@@ -1,6 +1,6 @@
-import {FillExtrusionLayoutArray} from '../array_types.g';
+import {FillExtrusionLayoutArray, PosArray} from '../array_types.g';
-import {members as layoutAttributes} from './fill_extrusion_attributes';
+import {members as layoutAttributes, centroidAttributes} from './fill_extrusion_attributes';
import SegmentVector from '../segment';
import {ProgramConfigurationSet} from '../program_configuration';
import {TriangleIndexArray} from '../index_array_type';
@@ -63,6 +63,9 @@ class FillExtrusionBucket implements Bucket {
layoutVertexArray: FillExtrusionLayoutArray;
layoutVertexBuffer: VertexBuffer;
+ centroidVertexArray: PosArray;
+ centroidVertexBuffer: VertexBuffer;
+
indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer;
@@ -81,11 +84,11 @@ class FillExtrusionBucket implements Bucket {
this.hasPattern = false;
this.layoutVertexArray = new FillExtrusionLayoutArray();
+ this.centroidVertexArray = new PosArray();
this.indexArray = new TriangleIndexArray();
this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom);
this.segments = new SegmentVector();
this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
-
}
populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) {
@@ -131,7 +134,7 @@ class FillExtrusionBucket implements Bucket {
}
isEmpty() {
- return this.layoutVertexArray.length === 0;
+ return this.layoutVertexArray.length === 0 && this.centroidVertexArray.length === 0;
}
uploadPending() {
@@ -141,6 +144,7 @@ class FillExtrusionBucket implements Bucket {
upload(context: Context) {
if (!this.uploaded) {
this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, layoutAttributes);
+ this.centroidVertexBuffer = context.createVertexBuffer(this.centroidVertexArray, centroidAttributes.members, true);
this.indexBuffer = context.createIndexBuffer(this.indexArray);
}
this.programConfigurations.upload(context);
@@ -153,9 +157,11 @@ class FillExtrusionBucket implements Bucket {
this.indexBuffer.destroy();
this.programConfigurations.destroy();
this.segments.destroy();
+ this.centroidVertexBuffer.destroy();
}
addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) {
+ const centroid = {x: 0, y: 0, vertexCount: 0};
for (const polygon of classifyRings(geometry, EARCUT_MAX_RINGS)) {
let numVertices = 0;
for (const ring of polygon) {
@@ -191,11 +197,17 @@ class FillExtrusionBucket implements Bucket {
addVertex(this.layoutVertexArray, p1.x, p1.y, perp.x, perp.y, 0, 0, edgeDistance);
addVertex(this.layoutVertexArray, p1.x, p1.y, perp.x, perp.y, 0, 1, edgeDistance);
+ centroid.x += 2 * p1.x;
+ centroid.y += 2 * p1.y;
+ centroid.vertexCount += 2;
edgeDistance += dist;
addVertex(this.layoutVertexArray, p2.x, p2.y, perp.x, perp.y, 0, 0, edgeDistance);
addVertex(this.layoutVertexArray, p2.x, p2.y, perp.x, perp.y, 0, 1, edgeDistance);
+ centroid.x += 2 * p2.x;
+ centroid.y += 2 * p2.y;
+ centroid.vertexCount += 2;
const bottomRight = segment.vertexLength;
@@ -212,6 +224,7 @@ class FillExtrusionBucket implements Bucket {
}
}
}
+
}
if (segment.vertexLength + numVertices > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) {
@@ -240,10 +253,14 @@ class FillExtrusionBucket implements Bucket {
const p = ring[i];
addVertex(this.layoutVertexArray, p.x, p.y, 0, 0, 1, 1, 0);
+ centroid.x += p.x;
+ centroid.y += p.y;
+ centroid.vertexCount += 1;
flattened.push(p.x);
flattened.push(p.y);
}
+
}
const indices = earcut(flattened, holeIndices);
@@ -261,6 +278,13 @@ class FillExtrusionBucket implements Bucket {
segment.vertexLength += numVertices;
}
+ // remember polygon centroid to calculate elevation in GPU
+ for (let i = 0; i < centroid.vertexCount; i++) {
+ this.centroidVertexArray.emplaceBack(
+ Math.floor(centroid.x / centroid.vertexCount),
+ Math.floor(centroid.y / centroid.vertexCount)
+ );
+ }
this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, canonical);
}
}
diff --git a/src/data/dem_data.test.ts b/src/data/dem_data.test.ts
index 4380c0a7c57..9c68a9c4068 100644
--- a/src/data/dem_data.test.ts
+++ b/src/data/dem_data.test.ts
@@ -222,13 +222,26 @@ describe('DEMData is correctly serialized and deserialized', () => {
const dem0 = new DEMData('0', imageData0, 'mapbox');
const serialized = serialize(dem0);
+ // calculate min/max values
+ let min = Number.MAX_SAFE_INTEGER;
+ let max = Number.MIN_SAFE_INTEGER;
+ for (let x = 0; x < 4; x++) {
+ for (let y = 0; y < 4; y++) {
+ const ele = dem0.get(x, y);
+ if (ele > max) max = ele;
+ if (ele < min) min = ele;
+ }
+ }
+
expect(serialized).toEqual({
$name: 'DEMData',
uid: '0',
dim: 4,
stride: 6,
data: dem0.data,
- encoding: 'mapbox'
+ encoding: 'mapbox',
+ max,
+ min,
});
const transferrables = [];
diff --git a/src/data/dem_data.ts b/src/data/dem_data.ts
index cddb88be2e3..a6d0982ff9d 100644
--- a/src/data/dem_data.ts
+++ b/src/data/dem_data.ts
@@ -18,6 +18,8 @@ export default class DEMData {
data: Uint32Array;
stride: number;
dim: number;
+ min: number;
+ max: number;
encoding: 'mapbox' | 'terrarium';
// RGBAImage data has uniform 1px padding on all sides: square tile edge size defines stride
@@ -52,6 +54,17 @@ export default class DEMData {
this.data[this._idx(dim, -1)] = this.data[this._idx(dim - 1, 0)];
this.data[this._idx(-1, dim)] = this.data[this._idx(0, dim - 1)];
this.data[this._idx(dim, dim)] = this.data[this._idx(dim - 1, dim - 1)];
+
+ // calculate min/max values
+ this.min = Number.MAX_SAFE_INTEGER;
+ this.max = Number.MIN_SAFE_INTEGER;
+ for (let x = 0; x < dim; x++) {
+ for (let y = 0; y < dim; y++) {
+ const ele = this.get(x, y);
+ if (ele > this.max) this.max = ele;
+ if (ele < this.min) this.min = ele;
+ }
+ }
}
get(x: number, y: number) {
diff --git a/src/geo/transform.test.ts b/src/geo/transform.test.ts
index faab19fc50d..2969ea3efaf 100644
--- a/src/geo/transform.test.ts
+++ b/src/geo/transform.test.ts
@@ -3,6 +3,7 @@ import Transform from './transform';
import LngLat from './lng_lat';
import {OverscaledTileID, CanonicalTileID} from '../source/tile_id';
import {fixedLngLat, fixedCoord} from '../../test/unit/lib/fixed';
+import type Terrain from '../render/terrain';
describe('transform', () => {
test('creates a transform', () => {
@@ -340,7 +341,6 @@ describe('transform', () => {
});
test('maintains high float precision when calculating matrices', () => {
-
const transform = new Transform(0, 22, 0, 60, true);
transform.resize(200.25, 200.25);
transform.zoom = 20.25;
@@ -352,4 +352,59 @@ describe('transform', () => {
expect(transform.glCoordMatrix[0].toString().length).toBeGreaterThan(10);
expect(transform.maxPitchScaleFactor()).toBeCloseTo(2.366025418080343, 10);
});
+
+ test('recalcuateZoom', () => {
+ const transform = new Transform(0, 22, 0, 60, true);
+ transform.elevation = 200;
+ transform.center = new LngLat(10.0, 50.0);
+ transform.zoom = 14;
+ transform.resize(512, 512);
+
+ // expect same values because of no elevation change
+ transform.getElevation = () => 200;
+ transform.recalculateZoom(null);
+ expect(transform.zoom).toBe(14);
+
+ // expect new zoom because of elevation change
+ transform.getElevation = () => 400;
+ transform.recalculateZoom(null);
+ expect(transform.zoom).toBe(14.127997275621933);
+ expect(transform.elevation).toBe(400);
+ expect(transform._center.lng).toBe(10.00000000000071);
+ expect(transform._center.lat).toBe(50.00000000000017);
+ });
+
+ test('pointCoordinate with terrain when returning null should fall back to 2D', () => {
+ const transform = new Transform(0, 22, 0, 60, true);
+ transform.resize(500, 500);
+ const terrain = {
+ pointCoordinate: () => null
+ } as any as Terrain;
+ const coordinate = transform.pointCoordinate(new Point(0, 0), terrain);
+
+ expect(coordinate).toBeDefined();
+ });
+
+ test('horizon', () => {
+ const transform = new Transform(0, 22, 0, 85, true);
+ transform.resize(500, 500);
+ transform.pitch = 75;
+ const horizon = transform.getHorizon();
+
+ expect(horizon).toBeCloseTo(170.8176101748407, 10);
+ });
+
+ test('getBounds with horizon', () => {
+ const transform = new Transform(0, 22, 0, 85, true);
+ transform.resize(500, 500);
+
+ transform.pitch = 60;
+ expect(transform.getBounds().getNorthWest().toArray()).toStrictEqual(transform.pointLocation(new Point(0, 0)).toArray());
+
+ transform.pitch = 75;
+ const top = Math.max(0, transform.height / 2 - transform.getHorizon());
+ expect(top).toBeCloseTo(79.1823898251593, 10);
+ expect(transform.getBounds().getNorthWest().toArray()).toStrictEqual(transform.pointLocation(new Point(0, top)).toArray());
+ });
+
});
diff --git a/src/geo/transform.ts b/src/geo/transform.ts
index 06991e9f603..b6258942bb2 100644
--- a/src/geo/transform.ts
+++ b/src/geo/transform.ts
@@ -5,12 +5,13 @@ import Point from '@mapbox/point-geometry';
import {wrap, clamp} from '../util/util';
import {number as interpolate} from '../style-spec/util/interpolate';
import EXTENT from '../data/extent';
-import {vec4, mat4, mat2, vec2} from 'gl-matrix';
+import {vec3, vec4, mat4, mat2, vec2} from 'gl-matrix';
import {Aabb, Frustum} from '../util/primitives';
import EdgeInsets from './edge_insets';
import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id';
import type {PaddingOptions} from './edge_insets';
+import Terrain from '../render/terrain';
/**
* A single transform, generally used for a single tile to be
@@ -31,14 +32,17 @@ class Transform {
zoomFraction: number;
pixelsToGLUnits: [number, number];
cameraToCenterDistance: number;
+ cameraToSeaLevelDistance: number;
mercatorMatrix: mat4;
projMatrix: mat4;
invProjMatrix: mat4;
alignedProjMatrix: mat4;
pixelMatrix: mat4;
+ pixelMatrix3D: mat4;
pixelMatrixInverse: mat4;
glCoordMatrix: mat4;
labelPlaneMatrix: mat4;
+ freezeElevation: boolean;
_fov: number;
_pitch: number;
_zoom: number;
@@ -49,6 +53,8 @@ class Transform {
_minPitch: number;
_maxPitch: number;
_center: LngLat;
+ _elevation: number;
+ _pixelPerMeter: number;
_edgeInsets: EdgeInsets;
_constraining: boolean;
_posMatrixCache: {[_: string]: mat4};
@@ -57,6 +63,7 @@ class Transform {
constructor(minZoom?: number, maxZoom?: number, minPitch?: number, maxPitch?: number, renderWorldCopies?: boolean) {
this.tileSize = 512; // constant
this.maxValidLatitude = 85.051129; // constant
+ this.freezeElevation = false;
this._renderWorldCopies = renderWorldCopies === undefined ? true : !!renderWorldCopies;
this._minZoom = minZoom || 0;
@@ -70,6 +77,7 @@ class Transform {
this.width = 0;
this.height = 0;
this._center = new LngLat(0, 0);
+ this._elevation = 0;
this.zoom = 0;
this.angle = 0;
this._fov = 0.6435011087932844;
@@ -87,6 +95,7 @@ class Transform {
clone.width = this.width;
clone.height = this.height;
clone._center = this._center;
+ clone._elevation = this._elevation;
clone.zoom = this.zoom;
clone.angle = this.angle;
clone._fov = this._fov;
@@ -207,6 +216,14 @@ class Transform {
this._calcMatrices();
}
+ get elevation(): number { return this._elevation; }
+ set elevation(elevation: number) {
+ if (elevation === this._elevation) return;
+ this._elevation = elevation;
+ this._constrain();
+ this._calcMatrices();
+ }
+
get padding(): PaddingOptions { return this._edgeInsets.toJSON(); }
set padding(padding: PaddingOptions) {
if (this._edgeInsets.equals(padding)) return;
@@ -322,6 +339,7 @@ class Transform {
roundZoom?: boolean;
reparseOverscaled?: boolean;
renderWorldCopies?: boolean;
+ terrain?: Terrain;
}
): Array {
let z = this.coveringZoomLevel(options);
@@ -330,23 +348,24 @@ class Transform {
if (options.minzoom !== undefined && z < options.minzoom) return [];
if (options.maxzoom !== undefined && z > options.maxzoom) z = options.maxzoom;
+ const cameraCoord = this.pointCoordinate(this.getCameraPoint());
const centerCoord = MercatorCoordinate.fromLngLat(this.center);
const numTiles = Math.pow(2, z);
+ const cameraPoint = [numTiles * cameraCoord.x, numTiles * cameraCoord.y, 0];
const centerPoint = [numTiles * centerCoord.x, numTiles * centerCoord.y, 0];
const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invProjMatrix, this.worldSize, z);
// No change of LOD behavior for pitch lower than 60 and when there is no top padding: return only tile ids from the requested zoom level
let minZoom = options.minzoom || 0;
// Use 0.1 as an epsilon to avoid for explicit == 0.0 floating point checks
- if (this.pitch <= 60.0 && this._edgeInsets.top < 0.1)
+ if (!options.terrain && this.pitch <= 60.0 && this._edgeInsets.top < 0.1)
minZoom = z;
- // There should always be a certain number of maximum zoom level tiles surrounding the center location
- const radiusOfMaxLvlLodInTiles = 3;
+ // There should always be a certain number of maximum zoom level tiles surrounding the center location in 2D or in front of the camera in 3D
+ const radiusOfMaxLvlLodInTiles = options.terrain ? 2 / Math.min(this.tileSize, options.tileSize) * this.tileSize : 3;
const newRootTile = (wrap: number): any => {
return {
- // All tiles are on zero elevation plane => z difference is zero
aabb: new Aabb([wrap * numTiles, 0, 0], [(wrap + 1) * numTiles, numTiles, 0]),
zoom: 0,
x: 0,
@@ -388,8 +407,9 @@ class Transform {
fullyVisible = intersectResult === 2;
}
- const distanceX = it.aabb.distanceX(centerPoint);
- const distanceY = it.aabb.distanceY(centerPoint);
+ const refPoint = options.terrain ? cameraPoint : centerPoint;
+ const distanceX = it.aabb.distanceX(refPoint);
+ const distanceY = it.aabb.distanceY(refPoint);
const longestDim = Math.max(Math.abs(distanceX), Math.abs(distanceY));
// We're using distance based heuristics to determine if a tile should be split into quadrants or not.
@@ -401,9 +421,12 @@ class Transform {
// Have we reached the target depth or is the tile too far away to be any split further?
if (it.zoom === maxZoom || (longestDim > distToSplit && it.zoom >= minZoom)) {
+ const dz = maxZoom - it.zoom, dx = cameraPoint[0] - 0.5 - (x << dz), dy = cameraPoint[1] - 0.5 - (y << dz);
result.push({
tileID: new OverscaledTileID(it.zoom === maxZoom ? overscaledZ : it.zoom, it.wrap, it.zoom, x, y),
- distanceSq: vec2.sqrLen([centerPoint[0] - 0.5 - x, centerPoint[1] - 0.5 - y])
+ distanceSq: vec2.sqrLen([centerPoint[0] - 0.5 - x, centerPoint[1] - 0.5 - y]),
+ // this variable is currently not used, but may be important to reduce the amount of loaded tiles
+ tileDistanceToCamera: Math.sqrt(dx * dx + dy * dy)
});
continue;
}
@@ -411,8 +434,22 @@ class Transform {
for (let i = 0; i < 4; i++) {
const childX = (x << 1) + (i % 2);
const childY = (y << 1) + (i >> 1);
-
- stack.push({aabb: it.aabb.quadrant(i), zoom: it.zoom + 1, x: childX, y: childY, wrap: it.wrap, fullyVisible});
+ const childZ = it.zoom + 1;
+ let quadrant = it.aabb.quadrant(i);
+ if (options.terrain) {
+ const tileID = new OverscaledTileID(childZ, it.wrap, childZ, childX, childY);
+ const tile = options.terrain.getTerrainData(tileID).tile;
+ let minElevation = this.elevation, maxElevation = this.elevation;
+ if (tile && tile.dem) {
+ minElevation = tile.dem.min * options.terrain.exaggeration;
+ maxElevation = tile.dem.max * options.terrain.exaggeration;
+ }
+ quadrant = new Aabb(
+ [quadrant.min[0], quadrant.min[1], minElevation] as vec3,
+ [quadrant.max[0], quadrant.max[1], maxElevation] as vec3
+ );
+ }
+ stack.push({aabb: quadrant, zoom: childZ, x: childX, y: childY, wrap: it.wrap, fullyVisible});
}
}
@@ -446,6 +483,72 @@ class Transform {
get point(): Point { return this.project(this.center); }
+ /**
+ * Updates the center-elevation value unless freezeElevation is activated.
+ * @param terrain the terrain
+ */
+ updateElevation(terrain?: Terrain) {
+ if (this.freezeElevation) return;
+ this.elevation = terrain ? this.getElevation(this._center, terrain) : 0;
+ }
+
+ /**
+ * get the elevation from terrain for the current zoomlevel.
+ * @param lnglat the location
+ * @param terrain the terrain
+ * @returns {Number} elevation in meters
+ */
+ getElevation(lnglat: LngLat, terrain: Terrain) {
+ const merc = MercatorCoordinate.fromLngLat(lnglat);
+ const worldSize = (1 << this.tileZoom) * EXTENT;
+ const mercX = merc.x * worldSize, mercY = merc.y * worldSize;
+ const tileX = Math.floor(mercX / EXTENT), tileY = Math.floor(mercY / EXTENT);
+ const tileID = new OverscaledTileID(this.tileZoom, 0, this.tileZoom, tileX, tileY);
+ return terrain.getElevation(tileID, mercX % EXTENT, mercY % EXTENT, EXTENT);
+ }
+
+ /**
+ * get the camera position in LngLat and altitudes in meter
+ * @returns {Object} An object with lngLat & altitude.
+ */
+ getCameraPosition(): {
+ lngLat: LngLat;
+ altitude: number;
+ } {
+ const lngLat = this.pointLocation(this.getCameraPoint());
+ const altitude = Math.cos(this._pitch) * this.cameraToCenterDistance / this._pixelPerMeter;
+ return {lngLat, altitude: altitude + this.elevation};
+ }
+
+ /**
+ * This method works in combination with freezeElevation activated.
+ * freezeElevtion is enabled during map-panning because during this the camera should sit in constant height.
+ * After panning finished, call this method to recalculate the zoomlevel for the current camera-height in current terrain.
+ * @param {Terrain} terrain the terrain
+ */
+ recalculateZoom(terrain: Terrain) {
+ // find position the camera is looking on
+ const center = this.pointLocation(this.centerPoint, terrain);
+ const elevation = this.getElevation(center, terrain);
+ const deltaElevation = this.elevation - elevation;
+ if (!deltaElevation) return;
+
+ // calculate mercator distance between camera & target
+ const cameraPosition = this.getCameraPosition();
+ const camera = MercatorCoordinate.fromLngLat(cameraPosition.lngLat, cameraPosition.altitude);
+ const target = MercatorCoordinate.fromLngLat(center, elevation);
+ const dx = camera.x - target.x, dy = camera.y - target.y, dz = camera.z - target.z;
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // from this distance we calculate the new zoomlevel
+ const zoom = this.scaleZoom(this.cameraToCenterDistance / distance / this.tileSize);
+
+ // update matrices
+ this._elevation = elevation;
+ this._center = center;
+ this.zoom = zoom;
+ }
+
setLocationAtPoint(lnglat: LngLat, point: Point) {
const a = this.pointCoordinate(point);
const b = this.pointCoordinate(this.centerPoint);
@@ -462,31 +565,35 @@ class Transform {
/**
* Given a location, return the screen point that corresponds to it
* @param {LngLat} lnglat location
+ * @param {Terrain} terrain optional terrain
* @returns {Point} screen point
* @private
*/
- locationPoint(lnglat: LngLat) {
- return this.coordinatePoint(this.locationCoordinate(lnglat));
+ locationPoint(lnglat: LngLat, terrain?: Terrain): Point {
+ return terrain ?
+ this.coordinatePoint(this.locationCoordinate(lnglat), this.getElevation(lnglat, terrain), this.pixelMatrix3D) :
+ this.coordinatePoint(this.locationCoordinate(lnglat));
}
/**
* Given a point on screen, return its lnglat
* @param {Point} p screen point
+ * @param {Terrain} terrain optional terrain
* @returns {LngLat} lnglat location
* @private
*/
- pointLocation(p: Point) {
- return this.coordinateLocation(this.pointCoordinate(p));
+ pointLocation(p: Point, terrain?: Terrain): LngLat {
+ return this.coordinateLocation(this.pointCoordinate(p, terrain));
}
/**
* Given a geographical lnglat, return an unrounded
* coordinate that represents it at this transform's zoom level.
* @param {LngLat} lnglat
- * @returns {Coordinate}
+ * @returns {MercatorCoordinate}
* @private
*/
- locationCoordinate(lnglat: LngLat) {
+ locationCoordinate(lnglat: LngLat): MercatorCoordinate {
return MercatorCoordinate.fromLngLat(lnglat);
}
@@ -496,11 +603,27 @@ class Transform {
* @returns {LngLat} lnglat
* @private
*/
- coordinateLocation(coord: MercatorCoordinate) {
- return coord.toLngLat();
+ coordinateLocation(coord: MercatorCoordinate): LngLat {
+ return coord && coord.toLngLat();
}
- pointCoordinate(p: Point) {
+ /**
+ * Given a Point, return its mercator coordinate.
+ * @param {Point} p the point
+ * @param {Terrain} terrain optional terrain
+ * @returns {LngLat} lnglat
+ * @private
+ */
+ pointCoordinate(p: Point, terrain?: Terrain): MercatorCoordinate {
+ // get point-coordinate from terrain coordinates framebuffer
+ if (terrain) {
+ const coordinate = terrain.pointCoordinate(p);
+ if (coordinate != null) {
+ return coordinate;
+ }
+ }
+
+ // calcuate point-coordinate on flat earth
const targetZ = 0;
// since we don't know the correct projected z value for the point,
// unproject two points to get a line and then find the point on that
@@ -531,12 +654,14 @@ class Transform {
/**
* Given a coordinate, return the screen point that corresponds to it
* @param {Coordinate} coord
+ * @params {number} elevation default = 0
+ * @params {mat4} pixelMatrix, default = this.pixelMatrix
* @returns {Point} screen point
* @private
*/
- coordinatePoint(coord: MercatorCoordinate) {
- const p = [coord.x * this.worldSize, coord.y * this.worldSize, 0, 1] as any;
- vec4.transformMat4(p, p, this.pixelMatrix);
+ coordinatePoint(coord: MercatorCoordinate, elevation: number = 0, pixelMatrix = this.pixelMatrix): Point {
+ const p = [coord.x * this.worldSize, coord.y * this.worldSize, elevation, 1] as any;
+ vec4.transformMat4(p, p, pixelMatrix);
return new Point(p[0] / p[3], p[1] / p[3]);
}
@@ -546,9 +671,10 @@ class Transform {
* @returns {LngLatBounds} Returns a {@link LngLatBounds} object describing the map's geographical bounds.
*/
getBounds(): LngLatBounds {
+ const top = Math.max(0, this.height / 2 - this.getHorizon());
return new LngLatBounds()
- .extend(this.pointLocation(new Point(0, 0)))
- .extend(this.pointLocation(new Point(this.width, 0)))
+ .extend(this.pointLocation(new Point(0, top)))
+ .extend(this.pointLocation(new Point(this.width, top)))
.extend(this.pointLocation(new Point(this.width, this.height)))
.extend(this.pointLocation(new Point(0, this.height)));
}
@@ -564,6 +690,16 @@ class Transform {
return new LngLatBounds([this.lngRange[0], this.latRange[0]], [this.lngRange[1], this.latRange[1]]);
}
+ /**
+ * Calculate pixel height of the visible horizon in relation to map-center (e.g. height/2),
+ * multiplied by a static factor to simulate the earth-radius.
+ * The calculated value is the horizontal line from the camera-height to sea-level.
+ * @returns {number} Horizon above center in pixels.
+ */
+ getHorizon(): number {
+ return Math.tan(Math.PI / 2 - this._pitch) * this.cameraToCenterDistance * 0.85;
+ }
+
/**
* Sets or clears the map's geographical constraints.
* @param {LngLatBounds} bounds A {@link LngLatBounds} object describing the new geographic boundaries of the map.
@@ -682,7 +818,23 @@ class Transform {
const halfFov = this._fov / 2;
const offset = this.centerOffset;
+ const x = this.point.x, y = this.point.y;
this.cameraToCenterDistance = 0.5 / Math.tan(halfFov) * this.height;
+ this._pixelPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize;
+
+ let m = mat4.identity(new Float64Array(16) as any);
+ mat4.scale(m, m, [this.width / 2, -this.height / 2, 1]);
+ mat4.translate(m, m, [1, -1, 0]);
+ this.labelPlaneMatrix = m;
+
+ m = mat4.identity(new Float64Array(16) as any);
+ mat4.scale(m, m, [1, -1, 1]);
+ mat4.translate(m, m, [-1, -1, 0]);
+ mat4.scale(m, m, [2 / this.width, 2 / this.height, 1]);
+ this.glCoordMatrix = m;
+
+ // calculate the camera to sea-level distance in pixel in respect of terrain
+ this.cameraToSeaLevelDistance = this.cameraToCenterDistance + this._elevation * this._pixelPerMeter / Math.cos(this._pitch);
// Find the distance from the center point [width/2 + offset.x, height/2 + offset.y] to the
// center top point [width/2 + offset.x, 0] in Z units, using the law of sines.
@@ -690,14 +842,20 @@ class Transform {
// (the distance between[width/2, height/2] and [width/2 + 1, height/2])
const groundAngle = Math.PI / 2 + this._pitch;
const fovAboveCenter = this._fov * (0.5 + offset.y / this.height);
- const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * this.cameraToCenterDistance / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01));
- const point = this.point;
- const x = point.x, y = point.y;
+ const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * this.cameraToSeaLevelDistance / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01));
+
+ // Find the distance from the center point to the horizon
+ const horizon = this.getHorizon();
+ const horizonAngle = Math.atan(horizon / this.cameraToCenterDistance);
+ const fovCenterToHorizon = 2 * horizonAngle * (0.5 + offset.y / (horizon * 2));
+ const topHalfSurfaceDistanceHorizon = Math.sin(fovCenterToHorizon) * this.cameraToSeaLevelDistance / Math.sin(clamp(Math.PI - groundAngle - fovCenterToHorizon, 0.01, Math.PI - 0.01));
// Calculate z distance of the farthest fragment that should be rendered.
- const furthestDistance = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistance + this.cameraToCenterDistance;
+ const furthestDistance = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistance + this.cameraToSeaLevelDistance;
+ const furthestDistanceHorizon = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistanceHorizon + this.cameraToSeaLevelDistance;
+
// Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance`
- const farZ = furthestDistance * 1.01;
+ const farZ = Math.min(furthestDistance, furthestDistanceHorizon) * 1.01;
// The larger the value of nearZ is
// - the more depth precision is available for features (good)
@@ -709,10 +867,10 @@ class Transform {
const nearZ = this.height / 50;
// matrix for conversion from location to GL coordinates (-1 .. 1)
- let m = new Float64Array(16) as any;
+ m = new Float64Array(16) as any;
mat4.perspective(m, this._fov, this.width / this.height, nearZ, farZ);
- //Apply center of perspective offset
+ // Apply center of perspective offset
m[8] = -offset.x * 2 / this.width;
m[9] = offset.y * 2 / this.height;
@@ -727,10 +885,18 @@ class Transform {
this.mercatorMatrix = mat4.scale([] as any, m, [this.worldSize, this.worldSize, this.worldSize]);
// scale vertically to meters per pixel (inverse of ground resolution):
- mat4.scale(m, m, [1, 1, mercatorZfromAltitude(1, this.center.lat) * this.worldSize]);
+ mat4.scale(m, m, [1, 1, this._pixelPerMeter]);
+
+ // matrix for conversion from location to screen coordinates in 2D
+ this.pixelMatrix = mat4.multiply(new Float64Array(16) as any, this.labelPlaneMatrix, m);
+ // matrix for conversion from location to GL coordinates (-1 .. 1)
+ mat4.translate(m, m, [0, 0, -this.elevation]); // elevate camera over terrain
this.projMatrix = m;
- this.invProjMatrix = mat4.invert([] as any, this.projMatrix);
+ this.invProjMatrix = mat4.invert([] as any, m);
+
+ // matrix for conversion from location to screen coordinates in 2D
+ this.pixelMatrix3D = mat4.multiply(new Float64Array(16) as any, this.labelPlaneMatrix, m);
// Make a second projection matrix that is aligned to a pixel grid for rendering raster tiles.
// We're rounding the (floating point) x/y values to achieve to avoid rendering raster images to fractional
@@ -746,20 +912,6 @@ class Transform {
mat4.translate(alignedM, alignedM, [ dx > 0.5 ? dx - 1 : dx, dy > 0.5 ? dy - 1 : dy, 0 ]);
this.alignedProjMatrix = alignedM;
- m = mat4.create();
- mat4.scale(m, m, [this.width / 2, -this.height / 2, 1]);
- mat4.translate(m, m, [1, -1, 0]);
- this.labelPlaneMatrix = m;
-
- m = mat4.create();
- mat4.scale(m, m, [1, -1, 1]);
- mat4.translate(m, m, [-1, -1, 0]);
- mat4.scale(m, m, [2 / this.width, 2 / this.height, 1]);
- this.glCoordMatrix = m;
-
- // matrix for conversion from location to screen coordinates
- this.pixelMatrix = mat4.multiply(new Float64Array(16) as any, this.labelPlaneMatrix, this.projMatrix);
-
// inverse matrix for conversion from screen coordinaes to location
m = mat4.invert(new Float64Array(16) as any, this.pixelMatrix);
if (!m) throw new Error('failed to invert matrix');
diff --git a/src/index.ts b/src/index.ts
index 5aa5064373a..386118ab67e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,6 +8,7 @@ import AttributionControl from './ui/control/attribution_control';
import LogoControl from './ui/control/logo_control';
import ScaleControl from './ui/control/scale_control';
import FullscreenControl from './ui/control/fullscreen_control';
+import TerrainControl from './ui/control/terrain_control';
import Popup from './ui/popup';
import Marker from './ui/marker';
import Style from './style/style';
@@ -46,6 +47,7 @@ const exported = {
LogoControl,
ScaleControl,
FullscreenControl,
+ TerrainControl,
Popup,
Marker,
Style,
diff --git a/src/render/draw_background.ts b/src/render/draw_background.ts
index e54905b287a..116beae5f97 100644
--- a/src/render/draw_background.ts
+++ b/src/render/draw_background.ts
@@ -9,10 +9,11 @@ import {
import type Painter from './painter';
import type SourceCache from '../source/source_cache';
import type BackgroundStyleLayer from '../style/style_layer/background_style_layer';
+import {OverscaledTileID} from '../source/tile_id';
export default drawBackground;
-function drawBackground(painter: Painter, sourceCache: SourceCache, layer: BackgroundStyleLayer) {
+function drawBackground(painter: Painter, sourceCache: SourceCache, layer: BackgroundStyleLayer, coords?: Array) {
const color = layer.paint.get('background-color');
const opacity = layer.paint.get('background-opacity');
@@ -31,10 +32,8 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg
const stencilMode = StencilMode.disabled;
const depthMode = painter.depthModeForSublayer(0, pass === 'opaque' ? DepthMode.ReadWrite : DepthMode.ReadOnly);
const colorMode = painter.colorModeForRenderPass();
-
const program = painter.useProgram(image ? 'backgroundPattern' : 'background');
-
- const tileIDs = transform.coveringTiles({tileSize});
+ const tileIDs = coords ? coords : transform.coveringTiles({tileSize, terrain: painter.style.terrain});
if (image) {
context.activeTexture.set(gl.TEXTURE0);
@@ -43,13 +42,14 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg
const crossfade = layer.getCrossfadeParameters();
for (const tileID of tileIDs) {
- const matrix = painter.transform.calculatePosMatrix(tileID.toUnwrapped());
+ const matrix = coords ? tileID.posMatrix : painter.transform.calculatePosMatrix(tileID.toUnwrapped());
const uniformValues = image ?
backgroundPatternUniformValues(matrix, opacity, painter, image, {tileID, tileSize}, crossfade) :
backgroundUniformValues(matrix, opacity, color);
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(tileID);
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
- uniformValues, layer.id, painter.tileExtentBuffer,
+ uniformValues, terrainData, layer.id, painter.tileExtentBuffer,
painter.quadTriangleIndexBuffer, painter.tileExtentSegments);
}
}
diff --git a/src/render/draw_circle.ts b/src/render/draw_circle.ts
index 2ee94bbbb61..6e376f61a5d 100644
--- a/src/render/draw_circle.ts
+++ b/src/render/draw_circle.ts
@@ -15,6 +15,7 @@ import type VertexBuffer from '../gl/vertex_buffer';
import type IndexBuffer from '../gl/index_buffer';
import type {UniformValues} from './uniform_binding';
import type {CircleUniformsType} from './program/circle_program';
+import type {TerrainData} from '../render/terrain';
export default drawCircles;
@@ -24,6 +25,7 @@ type TileRenderState = {
layoutVertexBuffer: VertexBuffer;
indexBuffer: IndexBuffer;
uniformValues: UniformValues;
+ terrainData: TerrainData;
};
type SegmentsTileRenderState = {
@@ -66,6 +68,7 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt
const program = painter.useProgram('circle', programConfiguration);
const layoutVertexBuffer = bucket.layoutVertexBuffer;
const indexBuffer = bucket.indexBuffer;
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
const uniformValues = circleUniformValues(painter, coord, tile, layer);
const state: TileRenderState = {
@@ -74,6 +77,7 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt
layoutVertexBuffer,
indexBuffer,
uniformValues,
+ terrainData
};
if (sortFeaturesByKey) {
@@ -100,11 +104,11 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt
}
for (const segmentsState of segmentsRenderStates) {
- const {programConfiguration, program, layoutVertexBuffer, indexBuffer, uniformValues} = segmentsState.state;
+ const {programConfiguration, program, layoutVertexBuffer, indexBuffer, uniformValues, terrainData} = segmentsState.state;
const segments = segmentsState.segments;
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
- uniformValues, layer.id,
+ uniformValues, terrainData, layer.id,
layoutVertexBuffer, indexBuffer, segments,
layer.paint, painter.transform.zoom, programConfiguration);
}
diff --git a/src/render/draw_collision_debug.ts b/src/render/draw_collision_debug.ts
index 37cca5f5fe7..da85a777b8c 100644
--- a/src/render/draw_collision_debug.ts
+++ b/src/render/draw_collision_debug.ts
@@ -22,6 +22,7 @@ type TileBatch = {
circleOffset: number;
transform: mat4;
invTransform: mat4;
+ coord: OverscaledTileID;
};
let quadTriangles: QuadTriangleArray;
@@ -60,7 +61,8 @@ function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, layer: S
circleArray,
circleOffset,
transform,
- invTransform
+ invTransform,
+ coord
});
circleCount += circleArray.length / 4; // 4 values per circle
@@ -75,6 +77,7 @@ function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, layer: S
posMatrix,
painter.transform,
tile),
+ painter.style.terrain && painter.style.terrain.getTerrainData(coord),
layer.id, buffers.layoutVertexBuffer, buffers.indexBuffer,
buffers.segments, null, painter.transform.zoom, null, null,
buffers.collisionVertexBuffer);
@@ -132,6 +135,7 @@ function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, layer: S
painter.colorModeForRenderPass(),
CullFaceMode.disabled,
uniforms,
+ painter.style.terrain && painter.style.terrain.getTerrainData(batch.coord),
layer.id,
vertexBuffer,
indexBuffer,
diff --git a/src/render/draw_debug.ts b/src/render/draw_debug.ts
index 23250c50fd3..210e9858fc1 100644
--- a/src/render/draw_debug.ts
+++ b/src/render/draw_debug.ts
@@ -77,15 +77,12 @@ function drawDebugTile(painter, sourceCache, coord: OverscaledTileID) {
const stencilMode = StencilMode.disabled;
const colorMode = painter.colorModeForRenderPass();
const id = '$debug';
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
context.activeTexture.set(gl.TEXTURE0);
// Bind the empty texture for drawing outlines
painter.emptyTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
- program.draw(context, gl.LINE_STRIP, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
- debugUniformValues(posMatrix, Color.red), id,
- painter.debugBuffer, painter.tileBorderIndexBuffer, painter.debugSegments);
-
const tileRawData = sourceCache.getTileByID(coord.key).latestRawTileData;
const tileByteLength = (tileRawData && tileRawData.byteLength) || 0;
const tileSizeKb = Math.floor(tileByteLength / 1024);
@@ -99,8 +96,11 @@ function drawDebugTile(painter, sourceCache, coord: OverscaledTileID) {
drawTextToOverlay(painter, tileLabel);
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, ColorMode.alphaBlended, CullFaceMode.disabled,
- debugUniformValues(posMatrix, Color.transparent, scaleRatio), id,
+ debugUniformValues(posMatrix, Color.transparent, scaleRatio), null, id,
painter.debugBuffer, painter.quadTriangleIndexBuffer, painter.debugSegments);
+ program.draw(context, gl.LINE_STRIP, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
+ debugUniformValues(posMatrix, Color.red), terrainData, id,
+ painter.debugBuffer, painter.tileBorderIndexBuffer, painter.debugSegments);
}
function drawTextToOverlay(painter: Painter, text: string) {
diff --git a/src/render/draw_fill.ts b/src/render/draw_fill.ts
index 8f0af9f91db..e8a58733680 100644
--- a/src/render/draw_fill.ts
+++ b/src/render/draw_fill.ts
@@ -81,6 +81,7 @@ function drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode
const programConfiguration = bucket.programConfigurations.get(layer.id);
const program = painter.useProgram(programName, programConfiguration);
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
if (image) {
painter.context.activeTexture.set(gl.TEXTURE0);
@@ -96,7 +97,9 @@ function drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode
if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom);
}
- const tileMatrix = painter.translatePosMatrix(coord.posMatrix, tile,
+ const terrainCoord = terrainData ? coord : null;
+ const posMatrix = terrainCoord ? terrainCoord.posMatrix : coord.posMatrix;
+ const tileMatrix = painter.translatePosMatrix(posMatrix, tile,
layer.paint.get('fill-translate'), layer.paint.get('fill-translate-anchor'));
if (!isOutline) {
@@ -115,7 +118,7 @@ function drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode
}
program.draw(painter.context, drawMode, depthMode,
- painter.stencilModeForClipping(coord), colorMode, CullFaceMode.disabled, uniformValues,
+ painter.stencilModeForClipping(coord), colorMode, CullFaceMode.disabled, uniformValues, terrainData,
layer.id, bucket.layoutVertexBuffer, indexBuffer, segments,
layer.paint, painter.transform.zoom, programConfiguration);
}
diff --git a/src/render/draw_fill_extrusion.ts b/src/render/draw_fill_extrusion.ts
index 4080ae94f96..4eadce90026 100644
--- a/src/render/draw_fill_extrusion.ts
+++ b/src/render/draw_fill_extrusion.ts
@@ -58,6 +58,7 @@ function drawExtrusionTiles(painter, source, layer, coords, depthMode, stencilMo
const bucket: FillExtrusionBucket = (tile.getBucket(layer) as any);
if (!bucket) continue;
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
const programConfiguration = bucket.programConfigurations.get(layer.id);
const program = painter.useProgram(image ? 'fillExtrusionPattern' : 'fillExtrusion', programConfiguration);
@@ -86,8 +87,8 @@ function drawExtrusionTiles(painter, source, layer, coords, depthMode, stencilMo
fillExtrusionUniformValues(matrix, painter, shouldUseVerticalGradient, opacity);
program.draw(context, context.gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW,
- uniformValues, layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer,
+ uniformValues, terrainData, layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer,
bucket.segments, layer.paint, painter.transform.zoom,
- programConfiguration);
+ programConfiguration, painter.style.terrain && bucket.centroidVertexBuffer);
}
}
diff --git a/src/render/draw_heatmap.ts b/src/render/draw_heatmap.ts
index 1684480a402..108d38f01be 100644
--- a/src/render/draw_heatmap.ts
+++ b/src/render/draw_heatmap.ts
@@ -53,8 +53,7 @@ function drawHeatmap(painter: Painter, sourceCache: SourceCache, layer: HeatmapS
const {zoom} = painter.transform;
program.draw(context, gl.TRIANGLES, DepthMode.disabled, stencilMode, colorMode, CullFaceMode.disabled,
- heatmapUniformValues(coord.posMatrix,
- tile, zoom, layer.paint.get('heatmap-intensity')),
+ heatmapUniformValues(coord.posMatrix, tile, zoom, layer.paint.get('heatmap-intensity')), null,
layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer,
bucket.segments, layer.paint, painter.transform.zoom,
programConfiguration);
@@ -125,7 +124,7 @@ function renderTextureToMap(painter, layer) {
painter.useProgram('heatmapTexture').draw(context, gl.TRIANGLES,
DepthMode.disabled, StencilMode.disabled, painter.colorModeForRenderPass(), CullFaceMode.disabled,
- heatmapTextureUniformValues(painter, layer, 0, 1),
+ heatmapTextureUniformValues(painter, layer, 0, 1), null,
layer.id, painter.viewportBuffer, painter.quadTriangleIndexBuffer,
painter.viewportSegments, layer.paint, painter.transform.zoom);
}
diff --git a/src/render/draw_hillshade.ts b/src/render/draw_hillshade.ts
index 32443c17666..e5582ea17b0 100644
--- a/src/render/draw_hillshade.ts
+++ b/src/render/draw_hillshade.ts
@@ -27,32 +27,33 @@ function drawHillshade(painter: Painter, sourceCache: SourceCache, layer: Hillsh
for (const coord of coords) {
const tile = sourceCache.getTile(coord);
- if (tile.needsHillshadePrepare && painter.renderPass === 'offscreen') {
+ if (typeof tile.needsHillshadePrepare !== 'undefined' && tile.needsHillshadePrepare && painter.renderPass === 'offscreen') {
prepareHillshade(painter, tile, layer, depthMode, StencilMode.disabled, colorMode);
} else if (painter.renderPass === 'translucent') {
- renderHillshade(painter, tile, layer, depthMode, stencilModes[coord.overscaledZ], colorMode);
+ renderHillshade(painter, coord, tile, layer, depthMode, stencilModes[coord.overscaledZ], colorMode);
}
}
context.viewport.set([0, 0, painter.width, painter.height]);
}
-function renderHillshade(painter, tile, layer, depthMode, stencilMode, colorMode) {
+function renderHillshade(painter, coord, tile, layer, depthMode, stencilMode, colorMode) {
const context = painter.context;
const gl = context.gl;
const fbo = tile.fbo;
if (!fbo) return;
const program = painter.useProgram('hillshade');
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
context.activeTexture.set(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, fbo.colorAttachment.get());
- const uniformValues = hillshadeUniformValues(painter, tile, layer);
-
+ const terrainCoord = terrainData ? coord : null;
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
- uniformValues, layer.id, painter.rasterBoundsBuffer,
+ hillshadeUniformValues(painter, tile, layer, terrainCoord), terrainData, layer.id, painter.rasterBoundsBuffer,
painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments);
+
}
// hillshade rendering is done in two steps. the prepare step first calculates the slope of the terrain in the x and y
@@ -97,7 +98,7 @@ function prepareHillshade(painter, tile, layer, depthMode, stencilMode, colorMod
painter.useProgram('hillshadePrepare').draw(context, gl.TRIANGLES,
depthMode, stencilMode, colorMode, CullFaceMode.disabled,
hillshadeUniformPrepareValues(tile.tileID, dem),
- layer.id, painter.rasterBoundsBuffer,
+ null, layer.id, painter.rasterBoundsBuffer,
painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments);
tile.needsHillshadePrepare = false;
diff --git a/src/render/draw_line.ts b/src/render/draw_line.ts
index 1a3b95e058d..8f6a92672af 100644
--- a/src/render/draw_line.ts
+++ b/src/render/draw_line.ts
@@ -56,6 +56,7 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay
const prevProgram = painter.context.program.get();
const program = painter.useProgram(programId, programConfiguration);
const programChanged = firstTile || program.program !== prevProgram;
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
const constantPattern = patternProperty.constantOr(null);
if (constantPattern && tile.imageAtlas) {
@@ -65,10 +66,11 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay
if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom);
}
- const uniformValues = image ? linePatternUniformValues(painter, tile, layer, crossfade) :
- dasharray ? lineSDFUniformValues(painter, tile, layer, dasharray, crossfade) :
- gradient ? lineGradientUniformValues(painter, tile, layer, bucket.lineClipsArray.length) :
- lineUniformValues(painter, tile, layer);
+ const terrainCoord = terrainData ? coord : null;
+ const uniformValues = image ? linePatternUniformValues(painter, tile, layer, crossfade, terrainCoord) :
+ dasharray ? lineSDFUniformValues(painter, tile, layer, dasharray, crossfade, terrainCoord) :
+ gradient ? lineGradientUniformValues(painter, tile, layer, bucket.lineClipsArray.length, terrainCoord) :
+ lineUniformValues(painter, tile, layer, terrainCoord);
if (image) {
context.activeTexture.set(gl.TEXTURE0);
@@ -113,7 +115,7 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay
}
program.draw(context, gl.TRIANGLES, depthMode,
- painter.stencilModeForClipping(coord), colorMode, CullFaceMode.disabled, uniformValues,
+ painter.stencilModeForClipping(coord), colorMode, CullFaceMode.disabled, uniformValues, terrainData,
layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer, bucket.segments,
layer.paint, painter.transform.zoom, programConfiguration, bucket.layoutVertexBuffer2);
diff --git a/src/render/draw_raster.ts b/src/render/draw_raster.ts
index 47e6689b8fe..0e881ce8f05 100644
--- a/src/render/draw_raster.ts
+++ b/src/render/draw_raster.ts
@@ -39,12 +39,11 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
layer.paint.get('raster-opacity') === 1 ? DepthMode.ReadWrite : DepthMode.ReadOnly, gl.LESS);
const tile = sourceCache.getTile(coord);
- const posMatrix = painter.transform.calculatePosMatrix(coord.toUnwrapped(), align);
tile.registerFadeDuration(layer.paint.get('raster-fade-duration'));
const parentTile = sourceCache.findLoadedParent(coord, 0),
- fade = getFadeValues(tile, parentTile, sourceCache, layer, painter.transform);
+ fade = getFadeValues(tile, parentTile, sourceCache, layer, painter.transform, painter.style.terrain);
let parentScaleBy, parentTL;
@@ -64,24 +63,27 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST);
}
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
+ const terrainCoord = terrainData ? coord : null;
+ const posMatrix = terrainCoord ? terrainCoord.posMatrix : painter.transform.calculatePosMatrix(coord.toUnwrapped(), align);
const uniformValues = rasterUniformValues(posMatrix, parentTL || [0, 0], parentScaleBy || 1, fade, layer);
if (source instanceof ImageSource) {
program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.disabled,
- uniformValues, layer.id, source.boundsBuffer,
+ uniformValues, terrainData, layer.id, source.boundsBuffer,
painter.quadTriangleIndexBuffer, source.boundsSegments);
} else {
program.draw(context, gl.TRIANGLES, depthMode, stencilModes[coord.overscaledZ], colorMode, CullFaceMode.disabled,
- uniformValues, layer.id, painter.rasterBoundsBuffer,
+ uniformValues, terrainData, layer.id, painter.rasterBoundsBuffer,
painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments);
}
}
}
-function getFadeValues(tile, parentTile, sourceCache, layer, transform) {
+function getFadeValues(tile, parentTile, sourceCache, layer, transform, terrain) {
const fadeDuration = layer.paint.get('raster-fade-duration');
- if (fadeDuration > 0) {
+ if (!terrain && fadeDuration > 0) {
const now = browser.now();
const sinceTile = (now - tile.timeAdded) / fadeDuration;
const sinceParent = parentTile ? (now - parentTile.timeAdded) / fadeDuration : -1;
diff --git a/src/render/draw_symbol.test.ts b/src/render/draw_symbol.test.ts
index 2f6e4222e64..71970f79074 100644
--- a/src/render/draw_symbol.test.ts
+++ b/src/render/draw_symbol.test.ts
@@ -10,9 +10,13 @@ import drawSymbol from './draw_symbol';
import * as symbolProjection from '../symbol/projection';
import type ZoomHistory from '../style/zoom_history';
import type Map from '../ui/map';
-import type Transform from '../geo/transform';
+import Transform from '../geo/transform';
import type EvaluationParameters from '../style/evaluation_parameters';
import type {SymbolLayerSpecification} from '../style-spec/types.g';
+import Style from '../style/style';
+import TerrainSourceCache from '../source/terrain_source_cache';
+import {Evented} from '../util/evented';
+import {RequestManager} from '../util/request_manager';
jest.mock('./painter');
jest.mock('./program');
@@ -21,6 +25,22 @@ jest.mock('../source/tile');
jest.mock('../data/bucket/symbol_bucket');
jest.mock('../symbol/projection');
+class StubMap extends Evented {
+ transform: Transform;
+ painter: Painter;
+ _requestManager: RequestManager;
+
+ constructor() {
+ super();
+ this.transform = new Transform();
+ this._requestManager = {
+ transformRequest: (url) => {
+ return {url};
+ }
+ } as any as RequestManager;
+ }
+}
+
describe('drawSymbol', () => {
test('should not do anything', () => {
const mockPainter = new Painter(null, null);
@@ -37,12 +57,13 @@ describe('drawSymbol', () => {
painterMock.context = {
gl: {},
activeTexture: {
- set: () => {}
+ set: () => { }
}
} as any;
painterMock.renderPass = 'translucent';
painterMock.transform = {pitch: 0, labelPlaneMatrix: mat4.create()} as any as Transform;
painterMock.options = {} as any;
+ painterMock.style = {terrainSourceCache: {getTerrain: () => null}} as any as Style;
const layerSpec = {
id: 'mock-layer',
@@ -58,12 +79,12 @@ describe('drawSymbol', () => {
const tileId = new OverscaledTileID(1, 0, 1, 0, 0);
tileId.posMatrix = mat4.create();
- const programMock = new Program(null, null, null, null, null, null);
+ const programMock = new Program(null, null, null, null, null, null, null);
(painterMock.useProgram as jest.Mock).mockReturnValue(programMock);
const bucketMock = new SymbolBucket(null);
bucketMock.icon = {
programConfigurations: {
- get: () => {}
+ get: () => { }
},
segments: {
get: () => [1]
@@ -76,7 +97,7 @@ describe('drawSymbol', () => {
const tile = new Tile(tileId, 256);
tile.tileID = tileId;
tile.imageAtlasTexture = {
- bind: () => {}
+ bind: () => { }
} as any;
(tile.getBucket as jest.Mock).mockReturnValue(bucketMock);
const sourceCacheMock = new SourceCache(null, null, null);
@@ -94,7 +115,7 @@ describe('drawSymbol', () => {
painterMock.context = {
gl: {},
activeTexture: {
- set: () => {}
+ set: () => { }
}
} as any;
painterMock.renderPass = 'translucent';
@@ -119,12 +140,12 @@ describe('drawSymbol', () => {
const tileId = new OverscaledTileID(1, 0, 1, 0, 0);
tileId.posMatrix = mat4.create();
- const programMock = new Program(null, null, null, null, null, null);
+ const programMock = new Program(null, null, null, null, null, null, null);
(painterMock.useProgram as jest.Mock).mockReturnValue(programMock);
const bucketMock = new SymbolBucket(null);
bucketMock.icon = {
programConfigurations: {
- get: () => {}
+ get: () => { }
},
segments: {
get: () => [1]
@@ -137,16 +158,19 @@ describe('drawSymbol', () => {
const tile = new Tile(tileId, 256);
tile.tileID = tileId;
tile.imageAtlasTexture = {
- bind: () => {}
+ bind: () => { }
} as any;
(tile.getBucket as jest.Mock).mockReturnValue(bucketMock);
const sourceCacheMock = new SourceCache(null, null, null);
(sourceCacheMock.getTile as jest.Mock).mockReturnValue(tile);
sourceCacheMock.map = {showCollisionBoxes: false} as any as Map;
+ painterMock.style = {
+ terrainSourceCache: new TerrainSourceCache(new Style(new StubMap() as any as Map))
+ } as any as Style;
const spy = jest.spyOn(symbolProjection, 'updateLineLabels');
drawSymbol(painterMock, sourceCacheMock, layer, [tileId], null);
- expect(spy.mock.calls[0][8]).toBeFalsy(); // rotateToLine === false
+ expect(spy.mock.calls[0][9]).toBeFalsy(); // rotateToLine === false
});
});
diff --git a/src/render/draw_symbol.ts b/src/render/draw_symbol.ts
index b58ce6b7424..bdde9b1594b 100644
--- a/src/render/draw_symbol.ts
+++ b/src/render/draw_symbol.ts
@@ -9,7 +9,7 @@ import {mat4} from 'gl-matrix';
import StencilMode from '../gl/stencil_mode';
import DepthMode from '../gl/depth_mode';
import CullFaceMode from '../gl/cull_face_mode';
-import SymbolBucket, {addDynamicAttributes, SymbolBuffers} from '../data/bucket/symbol_bucket';
+import {addDynamicAttributes} from '../data/bucket/symbol_bucket';
import {getAnchorAlignment, WritingMode} from '../symbol/shaping';
import ONE_EM from '../symbol/one_em';
@@ -31,6 +31,9 @@ import type {OverscaledTileID} from '../source/tile_id';
import type {UniformValues} from './uniform_binding';
import type {SymbolSDFUniformsType} from '../render/program/symbol_program';
import type {CrossTileID, VariableOffset} from '../symbol/placement';
+import type SymbolBucket from '../data/bucket/symbol_bucket';
+import type {SymbolBuffers} from '../data/bucket/symbol_bucket';
+import type {TerrainData} from '../render/terrain';
import type {SymbolLayerSpecification} from '../style-spec/types.g';
import type Transform from '../geo/transform';
import type ColorMode from '../gl/color_mode';
@@ -39,6 +42,7 @@ import type Program from './program';
type SymbolTileRenderState = {
segments: SegmentVector;
sortKey: number;
+ terrainData: TerrainData;
state: {
program: Program;
buffers: SymbolBuffers;
@@ -145,8 +149,9 @@ function updateVariableAnchors(coords: Array,
if (size) {
const tileScale = Math.pow(2, tr.zoom - tile.tileID.overscaledZ);
+ const getElevation = painter.style.terrain ? (x: number, y: number) => painter.style.terrain.getElevation(coord, x, y) : null;
updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, variableOffsets,
- tr, labelPlaneMatrix, coord.posMatrix, tileScale, size, updateTextFitIcon);
+ tr, labelPlaneMatrix, coord.posMatrix, tileScale, size, updateTextFitIcon, getElevation);
}
}
}
@@ -161,7 +166,8 @@ function updateVariableAnchorsForBucket(
posMatrix: mat4,
tileScale: number,
size: EvaluatedZoomSize,
- updateTextFitIcon: boolean) {
+ updateTextFitIcon: boolean,
+ getElevation: (x: number, y: number) => number) {
const placedSymbols = bucket.text.placedSymbolArray;
const dynamicTextLayoutVertexArray = bucket.text.dynamicLayoutVertexArray;
const dynamicIconLayoutVertexArray = bucket.icon.dynamicLayoutVertexArray;
@@ -179,7 +185,7 @@ function updateVariableAnchorsForBucket(
symbolProjection.hideGlyphs(symbol.numGlyphs, dynamicTextLayoutVertexArray);
} else {
const tileAnchor = new Point(symbol.anchorX, symbol.anchorY);
- const projectedAnchor = symbolProjection.project(tileAnchor, pitchWithMap ? posMatrix : labelPlaneMatrix);
+ const projectedAnchor = symbolProjection.project(tileAnchor, pitchWithMap ? posMatrix : labelPlaneMatrix, getElevation);
const perspectiveRatio = symbolProjection.getPerspectiveRatio(transform.cameraToCenterDistance, projectedAnchor.signedDistanceFromCamera);
let renderTextSize = evaluateSizeForFeature(bucket.textSizeData, size, symbol) * perspectiveRatio / ONE_EM;
if (pitchWithMap) {
@@ -196,7 +202,7 @@ function updateVariableAnchorsForBucket(
// calculated above. In the (somewhat weird) case of pitch-aligned text, we add an equivalent
// tile-unit based shift to the anchor before projecting to the label plane.
const shiftedAnchor = pitchWithMap ?
- symbolProjection.project(tileAnchor.add(shift), labelPlaneMatrix).point :
+ symbolProjection.project(tileAnchor.add(shift), labelPlaneMatrix, getElevation).point :
projectedAnchor.point.add(rotateWithMap ?
shift.rotate(-transform.angle) :
shift);
@@ -295,6 +301,7 @@ function drawLayerSymbols(
const program = painter.useProgram(getSymbolProgramName(isSDF, isText, bucket), programConfiguration);
const size = evaluateSizeForZoom(sizeData, tr.zoom);
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
let texSize: [number, number];
let texSizeIcon: [number, number] = [0, 0];
@@ -331,8 +338,9 @@ function drawLayerSymbols(
bucket.hasIconData();
if (alongLine) {
+ const getElevation = painter.style.terrain ? (x: number, y: number) => painter.style.terrain.getElevation(coord, x, y) : null;
const rotateToLine = layer.layout.get('text-rotation-alignment') === 'map';
- symbolProjection.updateLineLabels(bucket, coord.posMatrix, painter, isText, labelPlaneMatrix, glCoordMatrix, pitchWithMap, keepUpright, rotateToLine);
+ symbolProjection.updateLineLabels(bucket, coord.posMatrix, painter, isText, labelPlaneMatrix, glCoordMatrix, pitchWithMap, keepUpright, rotateToLine, getElevation);
}
const matrix = painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor),
@@ -377,14 +385,16 @@ function drawLayerSymbols(
tileRenderState.push({
segments: new SegmentVector([segment]),
sortKey: segment.sortKey,
- state
+ state,
+ terrainData
});
}
} else {
tileRenderState.push({
segments: buffers.segments,
sortKey: 0,
- state
+ state,
+ terrainData
});
}
}
@@ -409,11 +419,11 @@ function drawLayerSymbols(
const uniformValues = state.uniformValues;
if (state.hasHalo) {
uniformValues['u_is_halo'] = 1;
- drawSymbolElements(state.buffers, segmentState.segments, layer, painter, state.program, depthMode, stencilMode, colorMode, uniformValues);
+ drawSymbolElements(state.buffers, segmentState.segments, layer, painter, state.program, depthMode, stencilMode, colorMode, uniformValues, segmentState.terrainData);
}
uniformValues['u_is_halo'] = 0;
}
- drawSymbolElements(state.buffers, segmentState.segments, layer, painter, state.program, depthMode, stencilMode, colorMode, state.uniformValues);
+ drawSymbolElements(state.buffers, segmentState.segments, layer, painter, state.program, depthMode, stencilMode, colorMode, state.uniformValues, segmentState.terrainData);
}
}
@@ -426,11 +436,12 @@ function drawSymbolElements(
depthMode: Readonly,
stencilMode: StencilMode,
colorMode: Readonly,
- uniformValues: UniformValues) {
+ uniformValues: UniformValues,
+ terrainData: TerrainData) {
const context = painter.context;
const gl = context.gl;
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
- uniformValues, layer.id, buffers.layoutVertexBuffer,
+ uniformValues, terrainData, layer.id, buffers.layoutVertexBuffer,
buffers.indexBuffer, segments, layer.paint,
painter.transform.zoom, buffers.programConfigurations.get(layer.id),
buffers.dynamicLayoutVertexBuffer, buffers.opacityVertexBuffer);
diff --git a/src/render/draw_terrain.ts b/src/render/draw_terrain.ts
new file mode 100644
index 00000000000..7ee2abb0cf6
--- /dev/null
+++ b/src/render/draw_terrain.ts
@@ -0,0 +1,123 @@
+import StencilMode from '../gl/stencil_mode';
+import DepthMode from '../gl/depth_mode';
+import {terrainUniformValues, terrainDepthUniformValues, terrainCoordsUniformValues} from './program/terrain_program';
+import type Painter from './painter';
+import type Tile from '../source/tile';
+import CullFaceMode from '../gl/cull_face_mode';
+import Texture from './texture';
+import Color from '../style-spec/util/color';
+import ColorMode from '../gl/color_mode';
+import Terrain from './terrain';
+
+/**
+ * Redraw the Depth Framebuffer
+ * @param {Painter} painter - the painter
+ * @param {Terrain} terrain - the terrain
+ */
+function drawDepth(painter: Painter, terrain: Terrain) {
+ const context = painter.context;
+ const gl = context.gl;
+ const colorMode = ColorMode.unblended;
+ const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, [0, 1]);
+ const mesh = terrain.getTerrainMesh();
+ const tiles = terrain.sourceCache.getRenderableTiles();
+ const program = painter.useProgram('terrainDepth');
+ context.bindFramebuffer.set(terrain.getFramebuffer('depth').framebuffer);
+ context.viewport.set([0, 0, painter.width / devicePixelRatio, painter.height / devicePixelRatio]);
+ context.clear({color: Color.transparent, depth: 1});
+ for (const tile of tiles) {
+ const terrainData = terrain.getTerrainData(tile.tileID);
+ const posMatrix = painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped());
+ const uniformValues = terrainDepthUniformValues(posMatrix);
+ program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, 'terrain', mesh.vertexBuffer, mesh.indexBuffer, mesh.segments);
+ }
+ context.bindFramebuffer.set(null);
+ context.viewport.set([0, 0, painter.width, painter.height]);
+}
+
+/**
+ * Redraw the Coords Framebuffers
+ * @param {Painter} painter - the painter
+ * @param {Terrain} terrain - the terrain
+ */
+function drawCoords(painter: Painter, terrain: Terrain) {
+ const context = painter.context;
+ const gl = context.gl;
+ const colorMode = ColorMode.unblended;
+ const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, [0, 1]);
+ const mesh = terrain.getTerrainMesh();
+ const coords = terrain.getCoordsTexture();
+ const tiles = terrain.sourceCache.getRenderableTiles();
+
+ // draw tile-coords into framebuffer
+ const program = painter.useProgram('terrainCoords');
+ context.bindFramebuffer.set(terrain.getFramebuffer('coords').framebuffer);
+ context.viewport.set([0, 0, painter.width / devicePixelRatio, painter.height / devicePixelRatio]);
+ context.clear({color: Color.transparent, depth: 1});
+ terrain.coordsIndex = [];
+ for (const tile of tiles) {
+ const terrainData = terrain.getTerrainData(tile.tileID);
+ context.activeTexture.set(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, coords.texture);
+ const posMatrix = painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped());
+ const uniformValues = terrainCoordsUniformValues(posMatrix, 255 - terrain.coordsIndex.length);
+ program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, 'terrain', mesh.vertexBuffer, mesh.indexBuffer, mesh.segments);
+ terrain.coordsIndex.push(tile.tileID.key);
+ }
+
+ context.bindFramebuffer.set(null);
+ context.viewport.set([0, 0, painter.width, painter.height]);
+}
+
+/**
+ * Render, e.g. drape, a render-to-texture tile onto the 3d mesh on screen.
+ * @param {Painter} painter - the painter
+ * @param {Terrain} terrain - the source cache
+ * @param {Tile} tile - the tile
+ */
+function drawTerrain(painter: Painter, terrain: Terrain, tile: Tile) {
+ const context = painter.context;
+ const gl = context.gl;
+ const colorMode = painter.colorModeForRenderPass();
+ const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D);
+ const program = painter.useProgram('terrain');
+ const mesh = terrain.getTerrainMesh();
+ const terrainData = terrain.getTerrainData(tile.tileID);
+
+ context.bindFramebuffer.set(null);
+ context.viewport.set([0, 0, painter.width, painter.height]);
+ context.activeTexture.set(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, terrain.getRTTFramebuffer().colorAttachment.get());
+ const posMatrix = painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped());
+ const uniformValues = terrainUniformValues(posMatrix);
+ program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, 'terrain', mesh.vertexBuffer, mesh.indexBuffer, mesh.segments);
+}
+
+/**
+ * prepare the render-to-texture tile.
+ * E.g. creates the necessary textures and attach them to the render-to-texture-framebuffer.
+ * @param {Painter} painter - the painter
+ * @param {Terrain} terrain - the terrain
+ * @param {Tile} tile - the tile
+ * @param {number} stack number of a layer-groop. see painter.ts
+ */
+function prepareTerrain(painter: Painter, terrain: Terrain, tile: Tile, stack: number) {
+ const context = painter.context;
+ const size = tile.tileSize * terrain.qualityFactor;
+ if (!tile.textures[stack]) {
+ tile.textures[stack] = painter.getTileTexture(size) || new Texture(context, {width: size, height: size, data: null}, context.gl.RGBA);
+ tile.textures[stack].bind(context.gl.LINEAR, context.gl.CLAMP_TO_EDGE);
+ if (stack === 0) terrain.sourceCache.renderHistory.push(tile.tileID.key);
+ }
+ const fb = terrain.getRTTFramebuffer();
+ fb.colorAttachment.set(tile.textures[stack].texture);
+ context.bindFramebuffer.set(fb.framebuffer);
+ context.viewport.set([0, 0, size, size]);
+}
+
+export {
+ prepareTerrain,
+ drawTerrain,
+ drawDepth,
+ drawCoords
+};
diff --git a/src/render/painter.ts b/src/render/painter.ts
index e2837c4d3af..3feba23690a 100644
--- a/src/render/painter.ts
+++ b/src/render/painter.ts
@@ -1,5 +1,4 @@
import browser from '../util/browser';
-
import {mat4, vec3} from 'gl-matrix';
import SourceCache from '../source/source_cache';
import EXTENT from '../data/extent';
@@ -32,6 +31,8 @@ import raster from './draw_raster';
import background from './draw_background';
import debug, {drawDebugPadding} from './draw_debug';
import custom from './draw_custom';
+import {drawDepth, drawCoords} from './draw_terrain';
+import {OverscaledTileID} from '../source/tile_id';
const draw = {
symbol,
@@ -49,7 +50,6 @@ const draw = {
import type Transform from '../geo/transform';
import type Tile from '../source/tile';
-import type {OverscaledTileID} from '../source/tile_id';
import type Style from '../style/style';
import type StyleLayer from '../style/style_layer';
import type {CrossFaded} from '../style/properties';
@@ -61,6 +61,7 @@ import type IndexBuffer from '../gl/index_buffer';
import type {DepthRangeType, DepthMaskType, DepthFuncType} from '../gl/types';
import type ResolvedImage from '../style-spec/expression/types/resolved_image';
import type {RGBAImage} from '../util/image';
+import RenderToTexture from './render_to_texture';
export type RenderPass = 'offscreen' | 'opaque' | 'translucent';
@@ -125,11 +126,16 @@ class Painter {
emptyTexture: Texture;
debugOverlayTexture: Texture;
debugOverlayCanvas: HTMLCanvasElement;
+ // this object stores the current camera-matrix and the last render time
+ // of the terrain-facilitators. e.g. depth & coords framebuffers
+ // every time the camera-matrix changes the terrain-facilitators will be redrawn.
+ terrainFacilitator: {dirty: boolean; matrix: mat4; renderTime: number};
constructor(gl: WebGLRenderingContext, transform: Transform) {
this.context = new Context(gl);
this.transform = transform;
this._tileTextures = {};
+ this.terrainFacilitator = {dirty: true, matrix: mat4.create(), renderTime: 0};
this.setup();
@@ -240,7 +246,7 @@ class Painter {
this.useProgram('clippingMask').draw(context, gl.TRIANGLES,
DepthMode.disabled, this.stencilClearMode, ColorMode.disabled, CullFaceMode.disabled,
- clippingMaskUniformValues(matrix),
+ clippingMaskUniformValues(matrix), null,
'$clipping', this.viewportBuffer,
this.quadTriangleIndexBuffer, this.viewportSegments);
}
@@ -267,12 +273,13 @@ class Painter {
for (const tileID of tileIDs) {
const id = this._tileClippingMaskIDs[tileID.key] = this.nextStencilID++;
+ const terrainData = this.style.terrain && this.style.terrain.getTerrainData(tileID);
program.draw(context, gl.TRIANGLES, DepthMode.disabled,
// Tests will always pass, and ref value will be written to stencil buffer.
new StencilMode({func: gl.ALWAYS, mask: 0}, id, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE),
ColorMode.disabled, CullFaceMode.disabled, clippingMaskUniformValues(tileID.posMatrix),
- '$clipping', this.tileExtentBuffer,
+ terrainData, '$clipping', this.tileExtentBuffer,
this.quadTriangleIndexBuffer, this.tileExtentSegments);
}
}
@@ -371,6 +378,7 @@ class Painter {
const layerIds = this.style._order;
const sourceCaches = this.style.sourceCaches;
+ const renderToTexture = this.style.terrain && new RenderToTexture(this);
for (const id in sourceCaches) {
const sourceCache = sourceCaches[id];
@@ -399,6 +407,21 @@ class Painter {
}
}
+ if (renderToTexture) {
+ // this is disabled, because render-to-texture is rendering all layers from bottom to top.
+ this.opaquePassCutoff = 0;
+
+ // update coords/depth-framebuffer on camera movement, or tile reloading
+ const newTiles = this.style.terrain.sourceCache.tilesAfterTime(this.terrainFacilitator.renderTime);
+ if (this.terrainFacilitator.dirty || !mat4.equals(this.terrainFacilitator.matrix, this.transform.projMatrix) || newTiles.length) {
+ mat4.copy(this.terrainFacilitator.matrix, this.transform.projMatrix);
+ this.terrainFacilitator.renderTime = Date.now();
+ this.terrainFacilitator.dirty = false;
+ drawDepth(this, this.style.terrain);
+ drawCoords(this, this.style.terrain);
+ }
+ }
+
// Offscreen pass ===============================================
// We first do all rendering that requires rendering to a separate
// framebuffer, and then save those for rendering back to the map
@@ -427,15 +450,17 @@ class Painter {
// Opaque pass ===============================================
// Draw opaque layers top-to-bottom first.
- this.renderPass = 'opaque';
+ if (!renderToTexture) {
+ this.renderPass = 'opaque';
- for (this.currentLayer = layerIds.length - 1; this.currentLayer >= 0; this.currentLayer--) {
- const layer = this.style._layers[layerIds[this.currentLayer]];
- const sourceCache = sourceCaches[layer.source];
- const coords = coordsAscending[layer.source];
+ for (this.currentLayer = layerIds.length - 1; this.currentLayer >= 0; this.currentLayer--) {
+ const layer = this.style._layers[layerIds[this.currentLayer]];
+ const sourceCache = sourceCaches[layer.source];
+ const coords = coordsAscending[layer.source];
- this._renderTileClippingMasks(layer, coords);
- this.renderLayer(this, sourceCache, layer, coords);
+ this._renderTileClippingMasks(layer, coords);
+ this.renderLayer(this, sourceCache, layer, coords);
+ }
}
// Translucent pass ===============================================
@@ -446,6 +471,8 @@ class Painter {
const layer = this.style._layers[layerIds[this.currentLayer]];
const sourceCache = sourceCaches[layer.source];
+ if (renderToTexture && renderToTexture.renderLayer(layer)) continue;
+
// For symbol layers in the translucent pass, we add extra tiles to the renderable set
// for cross-tile symbol fading. Symbol layers don't use tile clipping, so no need to render
// separate clipping masks
@@ -486,7 +513,7 @@ class Painter {
renderLayer(painter: Painter, sourceCache: SourceCache, layer: StyleLayer, coords: Array) {
if (layer.isHidden(this.transform.zoom)) return;
- if (layer.type !== 'background' && layer.type !== 'custom' && !coords.length) return;
+ if (layer.type !== 'background' && layer.type !== 'custom' && !(coords || []).length) return;
this.id = layer.id;
this.gpuTimingStart(layer);
@@ -600,9 +627,20 @@ class Painter {
useProgram(name: string, programConfiguration?: ProgramConfiguration | null): Program {
this.cache = this.cache || {};
- const key = `${name}${programConfiguration ? programConfiguration.cacheKey : ''}${this._showOverdrawInspector ? '/overdraw' : ''}`;
+ const key = name +
+ (programConfiguration ? programConfiguration.cacheKey : '') +
+ (this._showOverdrawInspector ? '/overdraw' : '') +
+ (this.style.terrain ? '/terrain' : '');
if (!this.cache[key]) {
- this.cache[key] = new Program(this.context, name, shaders[name], programConfiguration, programUniforms[name], this._showOverdrawInspector);
+ this.cache[key] = new Program(
+ this.context,
+ name,
+ shaders[name],
+ programConfiguration,
+ programUniforms[name],
+ this._showOverdrawInspector,
+ this.style.terrain
+ );
}
return this.cache[key];
}
diff --git a/src/render/program.ts b/src/render/program.ts
index f42aa2920f6..5427b4286aa 100644
--- a/src/render/program.ts
+++ b/src/render/program.ts
@@ -13,6 +13,9 @@ import type ColorMode from '../gl/color_mode';
import type CullFaceMode from '../gl/cull_face_mode';
import type {UniformBindings, UniformValues, UniformLocations} from './uniform_binding';
import type {BinderUniform} from '../data/program_configuration';
+import {terrainPreludeUniforms, TerrainPreludeUniformsType} from './program/terrain_program';
+import type {TerrainData} from '../render/terrain';
+import Terrain from '../render/terrain';
export type DrawMode = WebGLRenderingContext['LINES'] | WebGLRenderingContext['TRIANGLES'] | WebGLRenderingContext['LINE_STRIP'];
@@ -31,6 +34,7 @@ class Program {
attributes: {[_: string]: number};
numAttributes: number;
fixedUniforms: Us;
+ terrainUniforms: TerrainPreludeUniformsType;
binderUniforms: Array;
failedToCreate: boolean;
@@ -44,7 +48,9 @@ class Program {
},
configuration: ProgramConfiguration,
fixedUniforms: (b: Context, a: UniformLocations) => Us,
- showOverdrawInspector: boolean) {
+ showOverdrawInspector: boolean,
+ terrain: Terrain) {
+
const gl = context.gl;
this.program = gl.createProgram();
@@ -52,10 +58,11 @@ class Program {
const dynamicAttrInfo = configuration ? configuration.getBinderAttributes() : [];
const allAttrInfo = staticAttrInfo.concat(dynamicAttrInfo);
+ const preludeUniformsInfo = shaders.prelude.staticUniforms ? getTokenizedAttributesAndUniforms(shaders.prelude.staticUniforms) : [];
const staticUniformsInfo = source.staticUniforms ? getTokenizedAttributesAndUniforms(source.staticUniforms) : [];
const dynamicUniformsInfo = configuration ? configuration.getBinderUniforms() : [];
// remove duplicate uniforms
- const uniformList = staticUniformsInfo.concat(dynamicUniformsInfo);
+ const uniformList = preludeUniformsInfo.concat(staticUniformsInfo).concat(dynamicUniformsInfo);
const allUniformsInfo = [];
for (const uniform of uniformList) {
if (allUniformsInfo.indexOf(uniform) < 0) allUniformsInfo.push(uniform);
@@ -65,7 +72,9 @@ class Program {
if (showOverdrawInspector) {
defines.push('#define OVERDRAW_INSPECTOR;');
}
-
+ if (terrain) {
+ defines.push('#define TERRAIN3D;');
+ }
const fragmentSource = defines.concat(shaders.prelude.fragmentSource, source.fragmentSource).join('\n');
const vertexSource = defines.concat(shaders.prelude.vertexSource, source.vertexSource).join('\n');
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
@@ -117,6 +126,7 @@ class Program {
}
this.fixedUniforms = fixedUniforms(context, uniformLocations);
+ this.terrainUniforms = terrainPreludeUniforms(context, uniformLocations);
this.binderUniforms = configuration ? configuration.getUniforms(context, uniformLocations) : [];
}
@@ -127,6 +137,7 @@ class Program {
colorMode: Readonly,
cullFaceMode: Readonly,
uniformValues: UniformValues,
+ terrain: TerrainData,
layerID: string,
layoutVertexBuffer: VertexBuffer,
indexBuffer: IndexBuffer,
@@ -135,7 +146,8 @@ class Program {
zoom?: number | null,
configuration?: ProgramConfiguration | null,
dynamicLayoutBuffer?: VertexBuffer | null,
- dynamicLayoutBuffer2?: VertexBuffer | null) {
+ dynamicLayoutBuffer2?: VertexBuffer | null,
+ dynamicLayoutBuffer3?: VertexBuffer | null) {
const gl = context.gl;
@@ -147,6 +159,17 @@ class Program {
context.setColorMode(colorMode);
context.setCullFace(cullFaceMode);
+ // set varaibles used by the 3d functions defined in _prelude.vertex.glsl
+ if (terrain) {
+ context.activeTexture.set(gl.TEXTURE2);
+ gl.bindTexture(gl.TEXTURE_2D, terrain.depthTexture);
+ context.activeTexture.set(gl.TEXTURE3);
+ gl.bindTexture(gl.TEXTURE_2D, terrain.texture);
+ for (const name in this.terrainUniforms) {
+ this.terrainUniforms[name].set(terrain[name]);
+ }
+ }
+
for (const name in this.fixedUniforms) {
this.fixedUniforms[name].set(uniformValues[name]);
}
@@ -173,7 +196,8 @@ class Program {
indexBuffer,
segment.vertexOffset,
dynamicLayoutBuffer,
- dynamicLayoutBuffer2
+ dynamicLayoutBuffer2,
+ dynamicLayoutBuffer3
);
gl.drawElements(
diff --git a/src/render/program/hillshade_program.ts b/src/render/program/hillshade_program.ts
index 5d09504e30c..81a29165d09 100644
--- a/src/render/program/hillshade_program.ts
+++ b/src/render/program/hillshade_program.ts
@@ -55,7 +55,12 @@ const hillshadePrepareUniforms = (context: Context, locations: UniformLocations)
'u_unpack': new Uniform4f(context, locations.u_unpack)
});
-const hillshadeUniformValues = (painter: Painter, tile: Tile, layer: HillshadeStyleLayer): UniformValues => {
+const hillshadeUniformValues = (
+ painter: Painter,
+ tile: Tile,
+ layer: HillshadeStyleLayer,
+ coord: OverscaledTileID
+): UniformValues => {
const shadow = layer.paint.get('hillshade-shadow-color');
const highlight = layer.paint.get('hillshade-highlight-color');
const accent = layer.paint.get('hillshade-accent-color');
@@ -67,7 +72,7 @@ const hillshadeUniformValues = (painter: Painter, tile: Tile, layer: HillshadeSt
}
const align = !painter.options.moving;
return {
- 'u_matrix': painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped(), align),
+ 'u_matrix': coord ? coord.posMatrix : painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped(), align),
'u_image': 0,
'u_latrange': getTileLatRange(painter, tile.tileID),
'u_light': [layer.paint.get('hillshade-exaggeration'), azimuthal],
diff --git a/src/render/program/line_program.ts b/src/render/program/line_program.ts
index 89d6b5bdb2f..4c383077318 100644
--- a/src/render/program/line_program.ts
+++ b/src/render/program/line_program.ts
@@ -10,6 +10,7 @@ import type {CrossFaded} from '../../style/properties';
import type LineStyleLayer from '../../style/style_layer/line_style_layer';
import type Painter from '../painter';
import type {CrossfadeParameters} from '../../style/evaluation_parameters';
+import {OverscaledTileID} from '../../source/tile_id';
export type LineUniformsType = {
'u_matrix': UniformMatrix4f;
@@ -93,11 +94,16 @@ const lineSDFUniforms = (context: Context, locations: UniformLocations): LineSDF
'u_mix': new Uniform1f(context, locations.u_mix)
});
-const lineUniformValues = (painter: Painter, tile: Tile, layer: LineStyleLayer): UniformValues => {
+const lineUniformValues = (
+ painter: Painter,
+ tile: Tile,
+ layer: LineStyleLayer,
+ coord: OverscaledTileID
+): UniformValues => {
const transform = painter.transform;
return {
- 'u_matrix': calculateMatrix(painter, tile, layer),
+ 'u_matrix': calculateMatrix(painter, tile, layer, coord),
'u_ratio': 1 / pixelsToTileUnits(tile, 1, transform.zoom),
'u_device_pixel_ratio': painter.pixelRatio,
'u_units_to_pixels': [
@@ -107,8 +113,14 @@ const lineUniformValues = (painter: Painter, tile: Tile, layer: LineStyleLayer):
};
};
-const lineGradientUniformValues = (painter: Painter, tile: Tile, layer: LineStyleLayer, imageHeight: number): UniformValues => {
- return extend(lineUniformValues(painter, tile, layer), {
+const lineGradientUniformValues = (
+ painter: Painter,
+ tile: Tile,
+ layer: LineStyleLayer,
+ imageHeight: number,
+ coord: OverscaledTileID
+): UniformValues => {
+ return extend(lineUniformValues(painter, tile, layer, coord), {
'u_image': 0,
'u_image_height': imageHeight,
});
@@ -118,12 +130,13 @@ const linePatternUniformValues = (
painter: Painter,
tile: Tile,
layer: LineStyleLayer,
- crossfade: CrossfadeParameters
+ crossfade: CrossfadeParameters,
+ coord: OverscaledTileID
): UniformValues => {
const transform = painter.transform;
const tileZoomRatio = calculateTileRatio(tile, transform);
return {
- 'u_matrix': calculateMatrix(painter, tile, layer),
+ 'u_matrix': calculateMatrix(painter, tile, layer, coord),
'u_texsize': tile.imageAtlasTexture.size,
// camera zoom ratio
'u_ratio': 1 / pixelsToTileUnits(tile, 1, transform.zoom),
@@ -143,7 +156,8 @@ const lineSDFUniformValues = (
tile: Tile,
layer: LineStyleLayer,
dasharray: CrossFaded>,
- crossfade: CrossfadeParameters
+ crossfade: CrossfadeParameters,
+ coord: OverscaledTileID
): UniformValues => {
const transform = painter.transform;
const lineAtlas = painter.lineAtlas;
@@ -157,7 +171,7 @@ const lineSDFUniformValues = (
const widthA = posA.width * crossfade.fromScale;
const widthB = posB.width * crossfade.toScale;
- return extend(lineUniformValues(painter, tile, layer), {
+ return extend(lineUniformValues(painter, tile, layer, coord), {
'u_patternscale_a': [tileRatio / widthA, -posA.height / 2],
'u_patternscale_b': [tileRatio / widthB, -posB.height / 2],
'u_sdfgamma': lineAtlas.width / (Math.min(widthA, widthB) * 256 * painter.pixelRatio) / 2,
@@ -172,9 +186,9 @@ function calculateTileRatio(tile: Tile, transform: Transform) {
return 1 / pixelsToTileUnits(tile, 1, transform.tileZoom);
}
-function calculateMatrix(painter, tile, layer) {
+function calculateMatrix(painter, tile, layer, coord) {
return painter.translatePosMatrix(
- tile.tileID.posMatrix,
+ coord ? coord.posMatrix : tile.tileID.posMatrix,
tile,
layer.paint.get('line-translate'),
layer.paint.get('line-translate-anchor')
diff --git a/src/render/program/program_uniforms.ts b/src/render/program/program_uniforms.ts
index 25f2271d9f9..f1723ccac78 100644
--- a/src/render/program/program_uniforms.ts
+++ b/src/render/program/program_uniforms.ts
@@ -10,6 +10,7 @@ import {lineUniforms, lineGradientUniforms, linePatternUniforms, lineSDFUniforms
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';
export const programUniforms = {
fillExtrusion: fillExtrusionUniforms,
@@ -36,5 +37,8 @@ export const programUniforms = {
symbolSDF: symbolSDFUniforms,
symbolTextAndIcon: symbolTextAndIconUniforms,
background: backgroundUniforms,
- backgroundPattern: backgroundPatternUniforms
+ backgroundPattern: backgroundPatternUniforms,
+ terrain: terrainUniforms,
+ terrainDepth: terrainDepthUniforms,
+ terrainCoords: terrainCoordsUniforms
};
diff --git a/src/render/program/terrain_program.ts b/src/render/program/terrain_program.ts
new file mode 100644
index 00000000000..2f7890307eb
--- /dev/null
+++ b/src/render/program/terrain_program.ts
@@ -0,0 +1,83 @@
+import {
+ Uniform1i,
+ Uniform1f,
+ Uniform4f,
+ UniformMatrix4f
+} from '../uniform_binding';
+import type Context from '../../gl/context';
+import type {UniformValues, UniformLocations} from '../../render/uniform_binding';
+import {mat4} from 'gl-matrix';
+
+export type TerrainPreludeUniformsType = {
+ 'u_depth': Uniform1i;
+ 'u_terrain': Uniform1i;
+ 'u_terrain_dim': Uniform1f;
+ 'u_terrain_matrix': UniformMatrix4f;
+ 'u_terrain_unpack': Uniform4f;
+ 'u_terrain_offset': Uniform1f;
+ 'u_terrain_exaggeration': Uniform1f;
+};
+
+export type TerrainUniformsType = {
+ 'u_matrix': UniformMatrix4f;
+ 'u_texture': Uniform1i;
+};
+
+export type TerrainDepthUniformsType = {
+ 'u_matrix': UniformMatrix4f;
+};
+
+export type TerrainCoordsUniformsType = {
+ 'u_matrix': UniformMatrix4f;
+ 'u_texture': Uniform1i;
+ 'u_terrain_coords_id': Uniform1f;
+};
+
+const terrainPreludeUniforms = (context: Context, locations: UniformLocations): TerrainPreludeUniformsType => ({
+ 'u_depth': new Uniform1i(context, locations.u_depth),
+ 'u_terrain': new Uniform1i(context, locations.u_terrain),
+ 'u_terrain_dim': new Uniform1f(context, locations.u_terrain_dim),
+ 'u_terrain_matrix': new UniformMatrix4f(context, locations.u_terrain_matrix),
+ 'u_terrain_unpack': new Uniform4f(context, locations.u_terrain_unpack),
+ 'u_terrain_offset': new Uniform1f(context, locations.u_terrain_offset),
+ 'u_terrain_exaggeration': new Uniform1f(context, locations.u_terrain_exaggeration)
+});
+
+const terrainUniforms = (context: Context, locations: UniformLocations): TerrainUniformsType => ({
+ 'u_matrix': new UniformMatrix4f(context, locations.u_matrix),
+ 'u_texture': new Uniform1i(context, locations.u_texture)
+});
+
+const terrainDepthUniforms = (context: Context, locations: UniformLocations): TerrainDepthUniformsType => ({
+ 'u_matrix': new UniformMatrix4f(context, locations.u_matrix)
+});
+
+const terrainCoordsUniforms = (context: Context, locations: UniformLocations): TerrainCoordsUniformsType => ({
+ 'u_matrix': new UniformMatrix4f(context, locations.u_matrix),
+ 'u_texture': new Uniform1i(context, locations.u_texture),
+ 'u_terrain_coords_id': new Uniform1f(context, locations.u_terrain_coords_id)
+});
+
+const terrainUniformValues = (
+ matrix: mat4
+): UniformValues => ({
+ 'u_matrix': matrix,
+ 'u_texture': 0
+});
+
+const terrainDepthUniformValues = (
+ matrix: mat4
+): UniformValues => ({
+ 'u_matrix': matrix
+});
+
+const terrainCoordsUniformValues = (
+ matrix: mat4,
+ coordsId: number
+): UniformValues => ({
+ 'u_matrix': matrix,
+ 'u_terrain_coords_id': coordsId / 255,
+ 'u_texture': 0
+});
+
+export {terrainUniforms, terrainDepthUniforms, terrainCoordsUniforms, terrainPreludeUniforms, terrainUniformValues, terrainDepthUniformValues, terrainCoordsUniformValues};
diff --git a/src/render/render_to_texture.test.ts b/src/render/render_to_texture.test.ts
new file mode 100644
index 00000000000..e4f5d1141c0
--- /dev/null
+++ b/src/render/render_to_texture.test.ts
@@ -0,0 +1,41 @@
+import RenderToTexture from './render_to_texture';
+import type Painter from './painter';
+import type LineStyleLayer from '../style/style_layer/line_style_layer';
+import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer';
+
+describe('render to texture', () => {
+ test('should render text after a line by not adding the text to the stack', () => {
+ const painterMock = {
+ style: {
+ terrain: {
+ sourceCache: {
+ getRenderableTiles: () => [],
+ removeOutdated: () => {}
+ },
+ clearRerenderCache: () => {}
+ },
+ _order: []
+ }
+ } as any as Painter;
+ const uut = new RenderToTexture(painterMock);
+ const lineLayer = {
+ id: 'maine-line',
+ type: 'line',
+ source: 'maine'
+ } as LineStyleLayer;
+ const symbolLayer = {
+ id: 'maine-text',
+ type: 'symbol',
+ source: 'maine',
+ layout: {
+ 'text-field': 'maine',
+ 'symbol-placement': 'line'
+ }
+ } as any as SymbolStyleLayer;
+
+ expect(uut.renderLayer(lineLayer)).toBeTruthy();
+ painterMock.style._order = ['maine-line', 'maine-text'];
+ painterMock.currentLayer = 1;
+ expect(uut.renderLayer(symbolLayer)).toBeFalsy();
+ });
+});
diff --git a/src/render/render_to_texture.ts b/src/render/render_to_texture.ts
new file mode 100644
index 00000000000..b2ad0071590
--- /dev/null
+++ b/src/render/render_to_texture.ts
@@ -0,0 +1,154 @@
+import Painter from './painter';
+import Tile from '../source/tile';
+import Color from '../style-spec/util/color';
+import {OverscaledTileID} from '../source/tile_id';
+import {prepareTerrain, drawTerrain} from './draw_terrain';
+import type StyleLayer from '../style/style_layer';
+
+export default class RenderToTexture {
+ painter: Painter;
+ // this object holds a lookup table which layers should rendered to texture
+ _renderToTexture: {[keyof in StyleLayer['type']]?: boolean};
+ // coordsDescendingInv contains a list of all tiles which should be rendered for one render-to-texture tile
+ // e.g. render 4 raster-tiles with size 256px to the 512px render-to-texture tile
+ _coordsDescendingInv: {[_: string]: {[_:string]: Array}} = {};
+ // create a string representation of all to tiles rendered to render-to-texture tiles
+ // this string representation is used to check if tile should be re-rendered.
+ _coordsDescendingInvStr: {[_: string]: {[_:string]: string}} = {};
+ // store for render-stacks
+ // a render stack is a set of layers which should be rendered into one texture
+ // every stylesheet can have multipe stacks. A new stack is created if layers which should
+ // not rendered to texture sit inbetween layers which should rendered to texture. e.g. hillshading or symbols
+ _stacks: Array>;
+ // remember the previous processed layer to check if a new stack is needed
+ _prevType: string;
+ // create a lookup which tiles should rendered to texture
+ _rerender: {[_: string]: boolean};
+ // a list of tiles that can potentially rendered
+ _renderableTiles: Array;
+
+ constructor(painter: Painter) {
+ this.painter = painter;
+ this._renderToTexture = {background: true, fill: true, line: true, raster: true};
+ this._coordsDescendingInv = {};
+ this._coordsDescendingInvStr = {};
+ this._stacks = [];
+ this._prevType = null;
+ this._rerender = {};
+ this._renderableTiles = painter.style.terrain.sourceCache.getRenderableTiles();
+ this._init();
+ }
+
+ _init() {
+ const style = this.painter.style;
+ const terrain = style.terrain;
+
+ // fill _coordsDescendingInv
+ for (const id in style.sourceCaches) {
+ this._coordsDescendingInv[id] = {};
+ const tileIDs = style.sourceCaches[id].getVisibleCoordinates();
+ for (const tileID of tileIDs) {
+ const keys = terrain.sourceCache.getTerrainCoords(tileID);
+ for (const key in keys) {
+ if (!this._coordsDescendingInv[id][key]) this._coordsDescendingInv[id][key] = [];
+ this._coordsDescendingInv[id][key].push(keys[key]);
+ }
+ }
+ }
+
+ // fill _coordsDescendingInvStr
+ for (const id of style._order) {
+ const layer = style._layers[id], source = layer.source;
+ if (this._renderToTexture[layer.type]) {
+ if (!this._coordsDescendingInvStr[source]) {
+ this._coordsDescendingInvStr[source] = {};
+ for (const key in this._coordsDescendingInv[source])
+ this._coordsDescendingInvStr[source][key] = this._coordsDescendingInv[source][key].map(c => c.key).sort().join();
+ }
+ }
+ }
+
+ // remove cached textures
+ this._renderableTiles.forEach(tile => {
+ for (const source in this._coordsDescendingInvStr) {
+ // rerender if there are more coords to render than in the last rendering
+ const coords = this._coordsDescendingInvStr[source][tile.tileID.key];
+ if (coords && coords !== tile.textureCoords[source]) tile.clearTextures(this.painter);
+ // rerender if tile is marked for rerender
+ if (terrain.needsRerender(source, tile.tileID)) tile.clearTextures(this.painter);
+ }
+ this._rerender[tile.tileID.key] = !tile.textures.length;
+ });
+ terrain.clearRerenderCache();
+ terrain.sourceCache.removeOutdated(this.painter);
+
+ return this;
+ }
+
+ /**
+ * due that switching textures is relatively slow, the render
+ * layer-by-layer context is not practicable. To bypass this problem
+ * this lines of code stack all layers and later render all at once.
+ * Because of the stylesheet possibility to mixing render-to-texture layers
+ * and 'live'-layers (f.e. symbols) it is necessary to create more stacks. For example
+ * a symbol-layer is in between of fill-layers.
+ * @param {StyleLayer} layer the layer to render
+ * @returns {boolean} if true layer is rendered to texture, otherwise false
+ */
+ renderLayer(layer: StyleLayer): boolean {
+ const type = layer.type;
+ const painter = this.painter;
+ const layerIds = painter.style._order;
+ const currentLayer = painter.currentLayer;
+ const isLastLayer = currentLayer + 1 === layerIds.length;
+
+ // remember background, fill, line & raster layer to render into a stack
+ if (this._renderToTexture[type]) {
+ if (!this._prevType || !this._renderToTexture[this._prevType]) this._stacks.push([]);
+ this._prevType = type;
+ this._stacks[this._stacks.length - 1].push(layerIds[currentLayer]);
+ // rendering is done later, all in once
+ if (!isLastLayer) return true;
+ }
+
+ // in case a stack is finished render all collected stack-layers into a texture
+ if (this._renderToTexture[this._prevType] || type === 'hillshade' || (this._renderToTexture[type] && isLastLayer)) {
+ this._prevType = type;
+ const stack = this._stacks.length - 1, layers = this._stacks[stack] || [];
+ for (const tile of this._renderableTiles) {
+ prepareTerrain(painter, painter.style.terrain, tile, stack);
+ if (this._rerender[tile.tileID.key]) {
+ painter.context.clear({color: Color.transparent});
+ for (let l = 0; l < layers.length; l++) {
+ const layer = painter.style._layers[layers[l]];
+ const coords = layer.source ? this._coordsDescendingInv[layer.source][tile.tileID.key] : [tile.tileID];
+ painter._renderTileClippingMasks(layer, coords);
+ painter.renderLayer(painter, painter.style.sourceCaches[layer.source], layer, coords);
+ if (layer.source) tile.textureCoords[layer.source] = this._coordsDescendingInvStr[layer.source][tile.tileID.key];
+ }
+ }
+ drawTerrain(painter, painter.style.terrain, tile);
+ }
+
+ // the hillshading layer is a special case because it changes on every camera-movement
+ // so rerender it in any case.
+ if (type === 'hillshade') {
+ this._stacks.push([layerIds[currentLayer]]);
+ for (const tile of this._renderableTiles) {
+ const coords = this._coordsDescendingInv[layer.source][tile.tileID.key];
+ prepareTerrain(painter, painter.style.terrain, tile, this._stacks.length - 1);
+ painter.context.clear({color: Color.transparent});
+ painter._renderTileClippingMasks(layer, coords);
+ painter.renderLayer(painter, painter.style.sourceCaches[layer.source], layer, coords);
+ drawTerrain(painter, painter.style.terrain, tile);
+ }
+ return true;
+ }
+
+ return this._renderToTexture[type];
+ }
+
+ return false;
+ }
+
+}
diff --git a/src/render/terrain.test.ts b/src/render/terrain.test.ts
new file mode 100644
index 00000000000..309c906219f
--- /dev/null
+++ b/src/render/terrain.test.ts
@@ -0,0 +1,53 @@
+import Point from '@mapbox/point-geometry';
+import Terrain from './terrain';
+import gl from 'gl';
+import Context from '../gl/context';
+import {RGBAImage} from '../util/image';
+import Texture from './texture';
+import type Style from '../style/style';
+import type SourceCache from '../source/source_cache';
+import type TerrainSourceCache from '../source/terrain_source_cache';
+import type {TerrainSpecification} from '../style-spec/types.g';
+
+describe('Terrain', () => {
+ test('pointCoordiate should not return null', () => {
+ const style = {
+ map: {
+ painter: {
+ context: new Context(gl(1, 1)),
+ width: 1,
+ height: 1
+ }
+ }
+ } as any as Style;
+ const sourceCache = {
+ getTileByID: (tileID) => {
+ if (tileID !== 'abcd') {
+ return null;
+ }
+ return {
+ tileID: {
+ canonical: {
+ x: 0,
+ y: 0,
+ z: 0
+ }
+ }
+ };
+ }
+ } as any as TerrainSourceCache;
+ const terrain = new Terrain(style, {} as any as SourceCache, {} as any as TerrainSpecification);
+ terrain.sourceCache = sourceCache;
+ const context = style.map.painter.context as Context;
+ const pixels = new Uint8Array([0, 0, 255, 255]);
+ const image = new RGBAImage({width: 1, height: 1}, pixels);
+ const imageTexture = new Texture(context, image, context.gl.RGBA);
+ terrain.getFramebuffer('coords'); // allow init of frame buffers
+ terrain._fboCoordsTexture.texture = imageTexture.texture;
+ terrain.coordsIndex.push('abcd');
+
+ const coordinate = terrain.pointCoordinate(new Point(0, 0));
+
+ expect(coordinate).not.toBeNull();
+ });
+});
diff --git a/src/render/terrain.ts b/src/render/terrain.ts
new file mode 100644
index 00000000000..19a841ce27a
--- /dev/null
+++ b/src/render/terrain.ts
@@ -0,0 +1,369 @@
+
+import Tile from '../source/tile';
+import {mat4, vec2} from 'gl-matrix';
+import {OverscaledTileID} from '../source/tile_id';
+import {RGBAImage} from '../util/image';
+import {warnOnce} from '../util/util';
+import {PosArray, TriangleIndexArray} from '../data/array_types.g';
+import posAttributes from '../data/pos_attributes';
+import SegmentVector from '../data/segment';
+import VertexBuffer from '../gl/vertex_buffer';
+import IndexBuffer from '../gl/index_buffer';
+import Style from '../style/style';
+import Texture from '../render/texture';
+import type Framebuffer from '../gl/framebuffer';
+import Point from '@mapbox/point-geometry';
+import MercatorCoordinate from '../geo/mercator_coordinate';
+import TerrainSourceCache from '../source/terrain_source_cache';
+import SourceCache from '../source/source_cache';
+import EXTENT from '../data/extent';
+import {number as mix} from '../style-spec/util/interpolate';
+import type {TerrainSpecification} from '../style-spec/types.g';
+
+/**
+ * This is the main class which handles most of the 3D Terrain logic. It has the follwing topics:
+ * 1) loads raster-dem tiles via the internal sourceCache this.sourceCache
+ * 2) creates a depth-framebuffer, which is used to calculate the visibility of coordinates
+ * 3) creates a coords-framebuffer, which is used the get to tile-coordinate for a screen-pixel
+ * 4) stores all render-to-texture tiles in the this.sourceCache._tiles
+ * 5) calculates the elevation for a spezific tile-coordinate
+ * 6) creates a terrain-mesh
+ *
+ * A note about the GPU resource-usage:
+ * Framebuffers:
+ * - one for the depth & coords framebuffer with the size of the map-div.
+ * - one for rendering a tile to texture with the size of tileSize (= 512x512).
+ * Textures:
+ * - one texture for an empty raster-dem tile with size 1x1
+ * - one texture for an empty depth-buffer, when terrain is disabled with size 1x1
+ * - one texture for an each loaded raster-dem with size of the source.tileSize
+ * - one texture for the coords-framebuffer with the size of the map-div.
+ * - one texture for the depth-framebuffer with the size of the map-div.
+ * - one texture for the encoded tile-coords with the size 2*tileSize (=1024x1024)
+ * - finally for each render-to-texture tile (= this._tiles) a set of textures
+ * for each render stack (The stack-concept is documented in painter.ts).
+ * Normally there exists 1-3 Textures per tile, depending on the stylesheet.
+ * Each Textures has the size 2*tileSize (= 1024x1024). Also there exists a
+ * cache of the last 150 newest rendered tiles.
+ *
+ */
+
+export type TerrainData = {
+ 'u_depth': number;
+ 'u_terrain': number;
+ 'u_terrain_dim': number;
+ 'u_terrain_matrix': mat4;
+ 'u_terrain_unpack': number[];
+ 'u_terrain_offset': number;
+ 'u_terrain_exaggeration': number;
+ texture: WebGLTexture;
+ depthTexture: WebGLTexture;
+ tile: Tile;
+}
+
+export type TerrainMesh = {
+ indexBuffer: IndexBuffer;
+ vertexBuffer: VertexBuffer;
+ segments: SegmentVector;
+}
+
+export default class Terrain {
+ // The style this terrain crresponds to
+ style: Style;
+ // the sourcecache this terrain is based on
+ sourceCache: TerrainSourceCache;
+ // the TerrainSpecification object passed to this instance
+ options: TerrainSpecification;
+ // define the meshSize per tile.
+ meshSize: number;
+ // multiplicator for the elevation. Used to make terrain more "extrem".
+ exaggeration: number;
+ // defines the global offset of putting negative elevations (e.g. dead-sea) into positive values.
+ elevationOffset: number;
+ // to not see pixels in the render-to-texture tiles it is good to render them bigger
+ // this number is the multiplicator (must be a power of 2) for the current tileSize.
+ // So to get good results with not too much memory footprint a value of 2 should be fine.
+ qualityFactor: number;
+ // holds the framebuffer object in size of the screen to render the coords & depth into a texture.
+ _fbo: Framebuffer;
+ _fboCoordsTexture: Texture;
+ _fboDepthTexture: Texture;
+ _emptyDepthTexture: Texture;
+ // 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;
+ // coords index contains a list of tileID.keys. This index is used to identify
+ // the tile via the alpha-cannel in the coords-texture.
+ // As the alpha-channel has 1 Byte a max of 255 tiles can rendered without an error.
+ coordsIndex: Array;
+ // tile-coords encoded in the rgb channel, _coordsIndex is in the alpha-channel.
+ _coordsTexture: Texture;
+ // accuracy of the coords. 2 * tileSize should be enoughth.
+ _coordsTextureSize: number;
+ // variables for an empty dem texture, which is used while the raster-dem tile is loading.
+ _emptyDemUnpack: number[];
+ _emptyDemTexture: Texture;
+ _emptyDemMatrix: mat4;
+ // as of overzooming of raster-dem tiles in high zoomlevels, this cache contains
+ // matrices to transform from vector-tile coords to raster-dem-tile coords.
+ _demMatrixCache: {[_: string]: { matrix: mat4; coord: OverscaledTileID }};
+ // because of overzooming raster-dem tiles this cache holds the corresponding
+ // framebuffer-object to render tiles to texture
+ _rttFramebuffer: Framebuffer;
+ // loading raster-dem tiles foreach render-to-texture tile results in loading
+ // a lot of terrain-dem tiles with very low visual advantage. So with this setting
+ // remember all tiles which contains new data for a spezific source and tile-key.
+ _rerender: {[_: string]: {[_: number]: boolean}};
+
+ constructor(style: Style, sourceCache: SourceCache, options: TerrainSpecification) {
+ this.style = style;
+ this.sourceCache = new TerrainSourceCache(sourceCache);
+ this.options = options;
+ this.exaggeration = typeof options.exaggeration === 'number' ? options.exaggeration : 1.0;
+ this.elevationOffset = typeof options.elevationOffset === 'number' ? options.elevationOffset : 450; // ~ dead-sea
+ this.qualityFactor = 2;
+ this.meshSize = 128;
+ this._demMatrixCache = {};
+ this.coordsIndex = [];
+ this._coordsTextureSize = 1024;
+ this.clearRerenderCache();
+ }
+
+ /**
+ * get the elevation-value from original dem-data for a given tile-coordinate
+ * @param {OverscaledTileID} tileID - the tile to get elevation for
+ * @param {number} x between 0 .. EXTENT
+ * @param {number} y between 0 .. EXTENT
+ * @param {number} extent optional, default 8192
+ * @returns {number} - the elevation
+ */
+ getDEMElevation(tileID: OverscaledTileID, x: number, y: number, extent: number = EXTENT): number {
+ if (!(x >= 0 && x < extent && y >= 0 && y < extent)) return this.elevationOffset;
+ let elevation = 0;
+ const terrain = this.getTerrainData(tileID);
+ if (terrain.tile && terrain.tile.dem) {
+ const pos = vec2.transformMat4([] as any, [x / extent * EXTENT, y / extent * EXTENT], terrain.u_terrain_matrix);
+ const coord = [ pos[0] * terrain.tile.dem.dim, pos[1] * terrain.tile.dem.dim ];
+ const c = [ Math.floor(coord[0]), Math.floor(coord[1]) ];
+ const tl = terrain.tile.dem.get(c[0], c[1]);
+ const tr = terrain.tile.dem.get(c[0], c[1] + 1);
+ const bl = terrain.tile.dem.get(c[0] + 1, c[1]);
+ const br = terrain.tile.dem.get(c[0] + 1, c[1] + 1);
+ elevation = mix(mix(tl, tr, coord[0] - c[0]), mix(bl, br, coord[0] - c[0]), coord[1] - c[1]);
+ }
+ return elevation;
+ }
+
+ rememberForRerender(source: string, tileID: OverscaledTileID) {
+ for (const key in this.sourceCache._tiles) {
+ const tile = this.sourceCache._tiles[key];
+ if (tile.tileID.equals(tileID) || tile.tileID.isChildOf(tileID)) {
+ if (source === this.sourceCache.sourceCache.id) tile.timeLoaded = Date.now();
+ this._rerender[source] = this._rerender[source] || {};
+ this._rerender[source][tile.tileID.key] = true;
+ }
+ }
+ }
+
+ needsRerender(source: string, tileID: OverscaledTileID) {
+ return this._rerender[source] && this._rerender[source][tileID.key];
+ }
+
+ clearRerenderCache() {
+ this._rerender = {};
+ }
+
+ /**
+ * get the Elevation for given coordinate in respect of elevationOffset and exaggeration.
+ * @param {OverscaledTileID} tileID - the tile id
+ * @param {number} x between 0 .. EXTENT
+ * @param {number} y between 0 .. EXTENT
+ * @param {number} extent optional, default 8192
+ * @returns {number} - the elevation
+ */
+ getElevation(tileID: OverscaledTileID, x: number, y: number, extent: number = EXTENT): number {
+ return (this.getDEMElevation(tileID, x, y, extent) + this.elevationOffset) * this.exaggeration;
+ }
+
+ /**
+ * returns a Terrain Object for a tile. Unless the tile corresponds to data (e.g. tile is loading), return a flat dem object
+ * @param {OverscaledTileID} tileID - the tile to get the terrain for
+ * @returns {TerrainData} the terrain data to use in the program
+ */
+ getTerrainData(tileID: OverscaledTileID): TerrainData {
+ // create empty DEM Obejcts, which will used while raster-dem tiles are loading.
+ // creates an empty depth-buffer texture which is needed, during the initialisation process of the 3d mesh..
+ if (!this._emptyDemTexture) {
+ const context = this.style.map.painter.context;
+ const image = new RGBAImage({width: 1, height: 1}, new Uint8Array(1 * 4));
+ this._emptyDepthTexture = new Texture(context, image, context.gl.RGBA, {premultiply: false});
+ this._emptyDemUnpack = [0, 0, 0, 0];
+ this._emptyDemTexture = new Texture(context, new RGBAImage({width: 1, height: 1}), context.gl.RGBA, {premultiply: false});
+ this._emptyDemTexture.bind(context.gl.NEAREST, context.gl.CLAMP_TO_EDGE);
+ this._emptyDemMatrix = mat4.identity([] as any);
+ }
+ // find covering dem tile and prepare demTexture
+ const sourceTile = this.sourceCache.getSourceTile(tileID, true);
+ if (sourceTile && sourceTile.dem && (!sourceTile.demTexture || sourceTile.needsTerrainPrepare)) {
+ const context = this.style.map.painter.context;
+ sourceTile.demTexture = this.style.map.painter.getTileTexture(sourceTile.dem.stride);
+ if (sourceTile.demTexture) sourceTile.demTexture.update(sourceTile.dem.getPixels(), {premultiply: false});
+ else sourceTile.demTexture = new Texture(context, sourceTile.dem.getPixels(), context.gl.RGBA, {premultiply: false});
+ sourceTile.demTexture.bind(context.gl.NEAREST, context.gl.CLAMP_TO_EDGE);
+ sourceTile.needsTerrainPrepare = false;
+ }
+ // create matrix for lookup in dem data
+ const matrixKey = sourceTile && (sourceTile + sourceTile.tileID.key) + tileID.key;
+ if (matrixKey && !this._demMatrixCache[matrixKey]) {
+ const maxzoom = this.sourceCache.sourceCache._source.maxzoom;
+ let dz = tileID.canonical.z - sourceTile.tileID.canonical.z;
+ if (tileID.overscaledZ > tileID.canonical.z) {
+ if (tileID.canonical.z >= maxzoom) dz = tileID.canonical.z - maxzoom;
+ else warnOnce('cannot calculate elevation if elevation maxzoom > source.maxzoom');
+ }
+ const dx = tileID.canonical.x - (tileID.canonical.x >> dz << dz);
+ const dy = tileID.canonical.y - (tileID.canonical.y >> dz << dz);
+ const demMatrix = mat4.fromScaling(new Float64Array(16) as any, [1 / (EXTENT << dz), 1 / (EXTENT << dz), 0]);
+ mat4.translate(demMatrix, demMatrix, [dx * EXTENT, dy * EXTENT, 0]);
+ this._demMatrixCache[tileID.key] = {matrix: demMatrix, coord: tileID};
+ }
+ // return uniform values & textures
+ return {
+ 'u_depth': 2,
+ 'u_terrain': 3,
+ 'u_terrain_dim': sourceTile && sourceTile.dem && sourceTile.dem.dim || 1,
+ 'u_terrain_matrix': matrixKey ? this._demMatrixCache[tileID.key].matrix : this._emptyDemMatrix,
+ 'u_terrain_unpack': sourceTile && sourceTile.dem && sourceTile.dem.getUnpackVector() || this._emptyDemUnpack,
+ 'u_terrain_offset': this.elevationOffset,
+ 'u_terrain_exaggeration': this.exaggeration,
+ texture: (sourceTile && sourceTile.demTexture || this._emptyDemTexture).texture,
+ depthTexture: (this._fboDepthTexture || this._emptyDepthTexture).texture,
+ tile: sourceTile
+ };
+ }
+
+ /**
+ * create the render-to-texture framebuffer
+ * @returns {Framebuffer} - the frame buffer
+ */
+ getRTTFramebuffer() {
+ const painter = this.style.map.painter;
+ if (!this._rttFramebuffer) {
+ const size = this.sourceCache.tileSize * this.qualityFactor;
+ this._rttFramebuffer = painter.context.createFramebuffer(size, size, true);
+ this._rttFramebuffer.depthAttachment.set(painter.context.createRenderbuffer(painter.context.gl.DEPTH_COMPONENT16, size, size));
+ }
+ return this._rttFramebuffer;
+ }
+
+ /**
+ * get a framebuffer as big as the map-div, which will be used to render depth & coords into a texture
+ * @param {string} texture - the texture
+ * @returns {Framebuffer} the frame buffer
+ */
+ getFramebuffer(texture: string): Framebuffer {
+ const painter = this.style.map.painter;
+ const width = painter.width / devicePixelRatio;
+ const height = painter.height / devicePixelRatio;
+ if (this._fbo && (this._fbo.width !== width || this._fbo.height !== height)) {
+ this._fbo.destroy();
+ this._fboCoordsTexture.destroy();
+ this._fboDepthTexture.destroy();
+ delete this._fbo;
+ delete this._fboDepthTexture;
+ delete this._fboCoordsTexture;
+ }
+ if (!this._fboCoordsTexture) {
+ this._fboCoordsTexture = new Texture(painter.context, {width, height, data: null}, painter.context.gl.RGBA, {premultiply: false});
+ this._fboCoordsTexture.bind(painter.context.gl.NEAREST, painter.context.gl.CLAMP_TO_EDGE);
+ }
+ if (!this._fboDepthTexture) {
+ this._fboDepthTexture = new Texture(painter.context, {width, height, data: null}, painter.context.gl.RGBA, {premultiply: false});
+ this._fboDepthTexture.bind(painter.context.gl.NEAREST, painter.context.gl.CLAMP_TO_EDGE);
+ }
+ if (!this._fbo) {
+ this._fbo = painter.context.createFramebuffer(width, height, true);
+ this._fbo.depthAttachment.set(painter.context.createRenderbuffer(painter.context.gl.DEPTH_COMPONENT16, width, height));
+ }
+ this._fbo.colorAttachment.set(texture === 'coords' ? this._fboCoordsTexture.texture : this._fboDepthTexture.texture);
+ return this._fbo;
+ }
+
+ /**
+ * create coords texture, needed to grab coordinates from canvas
+ * encode coords coordinate into 4 bytes:
+ * - 8 lower bits for x
+ * - 8 lower bits for y
+ * - 4 higher bits for x
+ * - 4 higher bits for y
+ * - 8 bits for coordsIndex (1 .. 255) (= number of terraintile), is later setted in draw_terrain uniform value
+ * @returns {Texture} - the texture
+ */
+ getCoordsTexture(): Texture {
+ const context = this.style.map.painter.context;
+ if (this._coordsTexture) return this._coordsTexture;
+ const data = new Uint8Array(this._coordsTextureSize * this._coordsTextureSize * 4);
+ for (let y = 0, i = 0; y < this._coordsTextureSize; y++) for (let x = 0; x < this._coordsTextureSize; x++, i += 4) {
+ data[i + 0] = x & 255;
+ data[i + 1] = y & 255;
+ data[i + 2] = ((x >> 8) << 4) | (y >> 8);
+ data[i + 3] = 0;
+ }
+ const image = new RGBAImage({width: this._coordsTextureSize, height: this._coordsTextureSize}, new Uint8Array(data.buffer));
+ const texture = new Texture(context, image, context.gl.RGBA, {premultiply: false});
+ texture.bind(context.gl.NEAREST, context.gl.CLAMP_TO_EDGE);
+ this._coordsTexture = texture;
+ return texture;
+ }
+
+ /**
+ * Reads a pixel from the coords-framebuffer and translate this to mercator.
+ * @param {Point} p Screen-Coordinate
+ * @returns {MercatorCoordinate} mercator coordinate for a screen pixel
+ */
+ pointCoordinate(p: Point): MercatorCoordinate {
+ const rgba = new Uint8Array(4);
+ const painter = this.style.map.painter, context = painter.context, gl = context.gl;
+ // grab coordinate pixel from coordinates framebuffer
+ context.bindFramebuffer.set(this.getFramebuffer('coords').framebuffer);
+ gl.readPixels(p.x, painter.height / devicePixelRatio - p.y - 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, rgba);
+ context.bindFramebuffer.set(null);
+ // decode coordinates (encoding see getCoordsTexture)
+ const x = rgba[0] + ((rgba[2] >> 4) << 8);
+ const y = rgba[1] + ((rgba[2] & 15) << 8);
+ const tileID = this.coordsIndex[255 - rgba[3]];
+ const tile = tileID && this.sourceCache.getTileByID(tileID);
+ if (!tile) return null;
+ const coordsSize = this._coordsTextureSize;
+ const worldSize = (1 << tile.tileID.canonical.z) * coordsSize;
+ return new MercatorCoordinate(
+ (tile.tileID.canonical.x * coordsSize + x) / worldSize,
+ (tile.tileID.canonical.y * coordsSize + y) / worldSize,
+ this.getElevation(tile.tileID, x, y, coordsSize)
+ );
+ }
+
+ /**
+ * create a regular mesh which will be used by all terrain-tiles
+ * @returns {TerrainMesh} - the created regular mesh
+ */
+ getTerrainMesh(): TerrainMesh {
+ if (this._mesh) return this._mesh;
+ const context = this.style.map.painter.context;
+ const vertexArray = new PosArray(), indexArray = new TriangleIndexArray();
+ const meshSize = this.meshSize, delta = EXTENT / meshSize, meshSize2 = meshSize * meshSize;
+ for (let y = 0; y <= meshSize; y++) for (let x = 0; x <= meshSize; x++)
+ vertexArray.emplaceBack(x * delta, y * delta);
+ for (let y = 0; y < meshSize2; y += meshSize + 1) for (let x = 0; x < meshSize; x++) {
+ indexArray.emplaceBack(x + y, meshSize + x + y + 1, meshSize + x + y + 2);
+ indexArray.emplaceBack(x + y, meshSize + x + y + 2, x + y + 1);
+ }
+ this._mesh = {
+ indexBuffer: context.createIndexBuffer(indexArray),
+ vertexBuffer: context.createVertexBuffer(vertexArray, posAttributes.members),
+ segments: SegmentVector.simpleSegment(0, 0, vertexArray.length, indexArray.length)
+ };
+ return this._mesh;
+ }
+
+}
diff --git a/src/render/vertex_array_object.ts b/src/render/vertex_array_object.ts
index 236dba4c18b..80d875d027d 100644
--- a/src/render/vertex_array_object.ts
+++ b/src/render/vertex_array_object.ts
@@ -14,6 +14,7 @@ class VertexArrayObject {
boundVertexOffset: number;
boundDynamicVertexBuffer: VertexBuffer;
boundDynamicVertexBuffer2: VertexBuffer;
+ boundDynamicVertexBuffer3: VertexBuffer;
vao: any;
constructor() {
@@ -33,7 +34,8 @@ class VertexArrayObject {
indexBuffer?: IndexBuffer | null,
vertexOffset?: number | null,
dynamicVertexBuffer?: VertexBuffer | null,
- dynamicVertexBuffer2?: VertexBuffer | null) {
+ dynamicVertexBuffer2?: VertexBuffer | null,
+ dynamicVertexBuffer3?: VertexBuffer | null) {
this.context = context;
@@ -52,11 +54,12 @@ class VertexArrayObject {
this.boundIndexBuffer !== indexBuffer ||
this.boundVertexOffset !== vertexOffset ||
this.boundDynamicVertexBuffer !== dynamicVertexBuffer ||
- this.boundDynamicVertexBuffer2 !== dynamicVertexBuffer2
+ this.boundDynamicVertexBuffer2 !== dynamicVertexBuffer2 ||
+ this.boundDynamicVertexBuffer3 !== dynamicVertexBuffer3
);
if (!context.extVertexArrayObject || isFreshBindRequired) {
- this.freshBind(program, layoutVertexBuffer, paintVertexBuffers, indexBuffer, vertexOffset, dynamicVertexBuffer, dynamicVertexBuffer2);
+ this.freshBind(program, layoutVertexBuffer, paintVertexBuffers, indexBuffer, vertexOffset, dynamicVertexBuffer, dynamicVertexBuffer2, dynamicVertexBuffer3);
} else {
context.bindVertexArrayOES.set(this.vao);
@@ -72,6 +75,10 @@ class VertexArrayObject {
if (dynamicVertexBuffer2) {
dynamicVertexBuffer2.bind();
}
+
+ if (dynamicVertexBuffer3) {
+ dynamicVertexBuffer3.bind();
+ }
}
}
@@ -81,7 +88,9 @@ class VertexArrayObject {
indexBuffer?: IndexBuffer | null,
vertexOffset?: number | null,
dynamicVertexBuffer?: VertexBuffer | null,
- dynamicVertexBuffer2?: VertexBuffer | null) {
+ dynamicVertexBuffer2?: VertexBuffer | null,
+ dynamicVertexBuffer3?: VertexBuffer | null) {
+
let numPrevAttributes;
const numNextAttributes = program.numAttributes;
@@ -102,6 +111,7 @@ class VertexArrayObject {
this.boundVertexOffset = vertexOffset;
this.boundDynamicVertexBuffer = dynamicVertexBuffer;
this.boundDynamicVertexBuffer2 = dynamicVertexBuffer2;
+ this.boundDynamicVertexBuffer3 = dynamicVertexBuffer3;
} else {
numPrevAttributes = context.currentNumAttributes || 0;
@@ -127,6 +137,9 @@ class VertexArrayObject {
if (dynamicVertexBuffer2) {
dynamicVertexBuffer2.enableAttributes(gl, program);
}
+ if (dynamicVertexBuffer3) {
+ dynamicVertexBuffer3.enableAttributes(gl, program);
+ }
layoutVertexBuffer.bind();
layoutVertexBuffer.setVertexAttribPointers(gl, program, vertexOffset);
@@ -146,6 +159,10 @@ class VertexArrayObject {
dynamicVertexBuffer2.bind();
dynamicVertexBuffer2.setVertexAttribPointers(gl, program, vertexOffset);
}
+ if (dynamicVertexBuffer3) {
+ dynamicVertexBuffer3.bind();
+ dynamicVertexBuffer3.setVertexAttribPointers(gl, program, vertexOffset);
+ }
context.currentNumAttributes = numNextAttributes;
}
diff --git a/src/shaders/_prelude.vertex.glsl b/src/shaders/_prelude.vertex.glsl
index 9cd030e4eb9..2ae1de99624 100644
--- a/src/shaders/_prelude.vertex.glsl
+++ b/src/shaders/_prelude.vertex.glsl
@@ -71,3 +71,79 @@ vec2 get_pattern_pos(const vec2 pixel_coord_upper, const vec2 pixel_coord_lower,
vec2 offset = mod(mod(mod(pixel_coord_upper, pattern_size) * 256.0, pattern_size) * 256.0 + pixel_coord_lower, pattern_size);
return (tile_units_to_pixels * pos + offset) / pattern_size;
}
+
+// logic for terrain 3d
+
+#ifdef TERRAIN3D
+uniform sampler2D u_terrain;
+uniform float u_terrain_dim;
+uniform mat4 u_terrain_matrix;
+uniform vec4 u_terrain_unpack;
+uniform float u_terrain_offset;
+uniform float u_terrain_exaggeration;
+uniform highp sampler2D u_depth;
+#endif
+
+// methods for pack/unpack depth value to texture rgba
+// https://stackoverflow.com/questions/34963366/encode-floating-point-data-in-a-rgba-texture
+const highp vec4 bitSh = vec4(256. * 256. * 256., 256. * 256., 256., 1.);
+const highp vec4 bitShifts = vec4(1.) / bitSh;
+
+highp float unpack(highp vec4 color) {
+ return dot(color , bitShifts);
+}
+
+// calculate the opacity behind terrain, returns a value between 0 and 1.
+highp float depthOpacity(vec3 frag) {
+ #ifdef TERRAIN3D
+ // create the delta between frag.z + terrain.z.
+ highp float d = unpack(texture2D(u_depth, frag.xy * 0.5 + 0.5)) + 0.0001 - frag.z;
+ // visibility range is between 0 and 0.002. 0 is visible, 0.002 is fully invisible.
+ return 1.0 - max(0.0, min(1.0, -d * 500.0));
+ #else
+ return 1.0;
+ #endif
+}
+
+// calculate the visibility of a coordinate in terrain and return an opacity value.
+// if a coordinate is behind the terrain reduce its opacity
+float calculate_visibility(vec4 pos) {
+ #ifdef TERRAIN3D
+ vec3 frag = pos.xyz / pos.w;
+ // check if coordingate is fully visible
+ highp float d = depthOpacity(frag);
+ if (d > 0.95) return 1.0;
+ // if not, go some pixel above and check it this point is visible
+ return (d + depthOpacity(frag + vec3(0.0, 0.01, 0.0))) / 2.0;
+ #else
+ return 1.0;
+ #endif
+}
+
+// grab an elevation value from a raster-dem texture
+float ele(vec2 pos) {
+ #ifdef TERRAIN3D
+ vec4 rgb = (texture2D(u_terrain, pos) * 255.0) * u_terrain_unpack;
+ return rgb.r + rgb.g + rgb.b - u_terrain_unpack.a;
+ #else
+ return 0.0;
+ #endif
+}
+
+// calculate the elevation with linear interpolation for a coordinate
+float get_elevation(vec2 pos) {
+ #ifdef TERRAIN3D
+ vec2 coord = (u_terrain_matrix * vec4(pos, 0.0, 1.0)).xy * u_terrain_dim + 1.0;
+ vec2 f = fract(coord);
+ vec2 c = (floor(coord) + 0.5) / (u_terrain_dim + 2.0); // get the pixel center
+ float d = 1.0 / (u_terrain_dim + 2.0);
+ float tl = ele(c);
+ float tr = ele(c + vec2(d, 0.0));
+ float bl = ele(c + vec2(0.0, d));
+ float br = ele(c + vec2(d, d));
+ float elevation = mix(mix(tl, tr, f.x), mix(bl, br, f.x), f.y);
+ return (elevation + u_terrain_offset) * u_terrain_exaggeration;
+ #else
+ return 0.0;
+ #endif
+}
diff --git a/src/shaders/circle.fragment.glsl b/src/shaders/circle.fragment.glsl
index 14081450b09..a9f972539fb 100644
--- a/src/shaders/circle.fragment.glsl
+++ b/src/shaders/circle.fragment.glsl
@@ -1,4 +1,5 @@
varying vec3 v_data;
+varying float v_visibility;
#pragma mapbox: define highp vec4 color
#pragma mapbox: define mediump float radius
@@ -31,7 +32,7 @@ void main() {
extrude_length - radius / (radius + stroke_width)
);
- gl_FragColor = opacity_t * mix(color * opacity, stroke_color * stroke_opacity, color_t);
+ gl_FragColor = v_visibility * opacity_t * mix(color * opacity, stroke_color * stroke_opacity, color_t);
#ifdef OVERDRAW_INSPECTOR
gl_FragColor = vec4(1.0);
diff --git a/src/shaders/circle.vertex.glsl b/src/shaders/circle.vertex.glsl
index 1f084104174..32f0ad2ee74 100644
--- a/src/shaders/circle.vertex.glsl
+++ b/src/shaders/circle.vertex.glsl
@@ -8,6 +8,7 @@ uniform highp float u_camera_to_center_distance;
attribute vec2 a_pos;
varying vec3 v_data;
+varying float v_visibility;
#pragma mapbox: define highp vec4 color
#pragma mapbox: define mediump float radius
@@ -32,6 +33,9 @@ void main(void) {
// multiply a_pos by 0.5, since we had it * 2 in order to sneak
// in extrusion data
vec2 circle_center = floor(a_pos * 0.5);
+ float ele = get_elevation(circle_center);
+ v_visibility = calculate_visibility(u_matrix * vec4(circle_center, ele, 1.0));
+
if (u_pitch_with_map) {
vec2 corner_position = circle_center;
if (u_scale_with_map) {
@@ -44,9 +48,9 @@ void main(void) {
corner_position += extrude * (radius + stroke_width) * u_extrude_scale * (projected_center.w / u_camera_to_center_distance);
}
- gl_Position = u_matrix * vec4(corner_position, 0, 1);
+ gl_Position = u_matrix * vec4(corner_position, ele, 1);
} else {
- gl_Position = u_matrix * vec4(circle_center, 0, 1);
+ gl_Position = u_matrix * vec4(circle_center, ele, 1);
if (u_scale_with_map) {
gl_Position.xy += extrude * (radius + stroke_width) * u_extrude_scale * u_camera_to_center_distance;
diff --git a/src/shaders/collision_box.vertex.glsl b/src/shaders/collision_box.vertex.glsl
index 8fa4bfc025d..6107e234b23 100644
--- a/src/shaders/collision_box.vertex.glsl
+++ b/src/shaders/collision_box.vertex.glsl
@@ -19,7 +19,7 @@ void main() {
0.0, // Prevents oversized near-field boxes in pitched/overzoomed tiles
4.0);
- gl_Position = u_matrix * vec4(a_pos, 0.0, 1.0);
+ gl_Position = u_matrix * vec4(a_pos, get_elevation(a_pos), 1.0);
gl_Position.xy += (a_extrude + a_shift) * u_extrude_scale * gl_Position.w * collision_perspective_ratio;
v_placed = a_placed.x;
diff --git a/src/shaders/debug.vertex.glsl b/src/shaders/debug.vertex.glsl
index c872e7fa981..f8635d8da0f 100644
--- a/src/shaders/debug.vertex.glsl
+++ b/src/shaders/debug.vertex.glsl
@@ -8,5 +8,5 @@ void main() {
// This vertex shader expects a EXTENT x EXTENT quad,
// The UV co-ordinates for the overlay texture can be calculated using that knowledge
v_uv = a_pos / 8192.0;
- gl_Position = u_matrix * vec4(a_pos * u_overlay_scale, 0, 1);
+ gl_Position = u_matrix * vec4(a_pos * u_overlay_scale, get_elevation(a_pos), 1);
}
diff --git a/src/shaders/fill_extrusion.vertex.glsl b/src/shaders/fill_extrusion.vertex.glsl
index 7a771b6dc97..836e141f0b0 100644
--- a/src/shaders/fill_extrusion.vertex.glsl
+++ b/src/shaders/fill_extrusion.vertex.glsl
@@ -8,6 +8,11 @@ uniform lowp float u_opacity;
attribute vec2 a_pos;
attribute vec4 a_normal_ed;
+#ifdef TERRAIN3D
+ attribute vec2 a_centroid;
+#endif
+
+
varying vec4 v_color;
#pragma mapbox: define highp float base
@@ -22,8 +27,17 @@ void main() {
vec3 normal = a_normal_ed.xyz;
- base = max(0.0, base);
- height = max(0.0, height);
+ #ifdef TERRAIN3D
+ // To avoid floating buildings in 3d-terrain, especially in heavy terrain,
+ // render the buildings a little below terrain. The unit is meter.
+ float baseDelta = 10.0;
+ float ele = get_elevation(a_centroid);
+ #else
+ float baseDelta = 0.0;
+ float ele = 0.0;
+ #endif
+ base = max(0.0, ele + base - baseDelta);
+ height = max(0.0, ele + height);
float t = mod(normal.x, 2.0);
diff --git a/src/shaders/fill_extrusion_pattern.vertex.glsl b/src/shaders/fill_extrusion_pattern.vertex.glsl
index 2428580f9ca..f04e109c910 100644
--- a/src/shaders/fill_extrusion_pattern.vertex.glsl
+++ b/src/shaders/fill_extrusion_pattern.vertex.glsl
@@ -13,6 +13,10 @@ uniform lowp float u_lightintensity;
attribute vec2 a_pos;
attribute vec4 a_normal_ed;
+#ifdef TERRAIN3D
+ attribute vec2 a_centroid;
+#endif
+
varying vec2 v_pos_a;
varying vec2 v_pos_b;
varying vec4 v_lighting;
@@ -47,8 +51,17 @@ void main() {
vec2 display_size_a = (pattern_br_a - pattern_tl_a) / pixel_ratio_from;
vec2 display_size_b = (pattern_br_b - pattern_tl_b) / pixel_ratio_to;
- base = max(0.0, base);
- height = max(0.0, height);
+ #ifdef TERRAIN3D
+ // To avoid floating buildings in 3d-terrain, especially in heavy terrain,
+ // render the buildings a little below terrain. The unit is meter.
+ float baseDelta = 10.0;
+ float ele = get_elevation(a_centroid);
+ #else
+ float baseDelta = 0.0;
+ float ele = 0.0;
+ #endif
+ base = max(0.0, ele + base - baseDelta);
+ height = max(0.0, ele + height);
float t = mod(normal.x, 2.0);
float z = t > 0.0 ? height : base;
diff --git a/src/shaders/line.vertex.glsl b/src/shaders/line.vertex.glsl
index 9334e2b72e1..13edbbe3864 100644
--- a/src/shaders/line.vertex.glsl
+++ b/src/shaders/line.vertex.glsl
@@ -77,9 +77,13 @@ void main() {
gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude;
// calculate how much the perspective view squishes or stretches the extrude
- float extrude_length_without_perspective = length(dist);
- float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels);
- v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective;
+ #ifdef TERRAIN3D
+ v_gamma_scale = 1.0; // not needed, because this is done automatically via the mesh
+ #else
+ float extrude_length_without_perspective = length(dist);
+ float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels);
+ v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective;
+ #endif
v_width2 = vec2(outset, inset);
}
diff --git a/src/shaders/line_gradient.vertex.glsl b/src/shaders/line_gradient.vertex.glsl
index 4b02ba5337e..244ddaea891 100644
--- a/src/shaders/line_gradient.vertex.glsl
+++ b/src/shaders/line_gradient.vertex.glsl
@@ -80,9 +80,13 @@ void main() {
gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude;
// calculate how much the perspective view squishes or stretches the extrude
- float extrude_length_without_perspective = length(dist);
- float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels);
- v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective;
+ #ifdef TERRAIN3D
+ v_gamma_scale = 1.0; // not needed, because this is done automatically via the mesh
+ #else
+ float extrude_length_without_perspective = length(dist);
+ float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels);
+ v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective;
+ #endif
v_width2 = vec2(outset, inset);
}
diff --git a/src/shaders/line_pattern.vertex.glsl b/src/shaders/line_pattern.vertex.glsl
index fff2daa110a..c1abacbab91 100644
--- a/src/shaders/line_pattern.vertex.glsl
+++ b/src/shaders/line_pattern.vertex.glsl
@@ -89,9 +89,13 @@ void main() {
gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude;
// calculate how much the perspective view squishes or stretches the extrude
- float extrude_length_without_perspective = length(dist);
- float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels);
- v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective;
+ #ifdef TERRAIN3D
+ v_gamma_scale = 1.0; // not needed, because this is done automatically via the mesh
+ #else
+ float extrude_length_without_perspective = length(dist);
+ float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels);
+ v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective;
+ #endif
v_linesofar = a_linesofar;
v_width2 = vec2(outset, inset);
diff --git a/src/shaders/line_sdf.vertex.glsl b/src/shaders/line_sdf.vertex.glsl
index c85140ef7c4..3dcb1e914c5 100644
--- a/src/shaders/line_sdf.vertex.glsl
+++ b/src/shaders/line_sdf.vertex.glsl
@@ -87,12 +87,15 @@ void main() {
gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude;
// calculate how much the perspective view squishes or stretches the extrude
- float extrude_length_without_perspective = length(dist);
- float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels);
- v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective;
+ #ifdef TERRAIN3D
+ v_gamma_scale = 1.0; // not needed, because this is done automatically via the mesh
+ #else
+ float extrude_length_without_perspective = length(dist);
+ float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels);
+ v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective;
+ #endif
v_tex_a = vec2(a_linesofar * u_patternscale_a.x / floorwidth, normal.y * u_patternscale_a.y + u_tex_y_a);
v_tex_b = vec2(a_linesofar * u_patternscale_b.x / floorwidth, normal.y * u_patternscale_b.y + u_tex_y_b);
-
v_width2 = vec2(outset, inset);
}
diff --git a/src/shaders/shaders.ts b/src/shaders/shaders.ts
index 404f514a5b5..d5cb000c077 100644
--- a/src/shaders/shaders.ts
+++ b/src/shaders/shaders.ts
@@ -1,3 +1,6 @@
+
+// Disable Flow annotations here because Flow doesn't support importing GLSL files
+
import preludeFrag from './_prelude.fragment.glsl.g';
import preludeVert from './_prelude.vertex.glsl.g';
import backgroundFrag from './background.fragment.glsl.g';
@@ -50,6 +53,10 @@ import symbolSDFFrag from './symbol_sdf.fragment.glsl.g';
import symbolSDFVert from './symbol_sdf.vertex.glsl.g';
import symbolTextAndIconFrag from './symbol_text_and_icon.fragment.glsl.g';
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 terrainVert from './terrain.vertex.glsl.g';
export default {
prelude: compile(preludeFrag, preludeVert),
@@ -77,7 +84,10 @@ export default {
raster: compile(rasterFrag, rasterVert),
symbolIcon: compile(symbolIconFrag, symbolIconVert),
symbolSDF: compile(symbolSDFFrag, symbolSDFVert),
- symbolTextAndIcon: compile(symbolTextAndIconFrag, symbolTextAndIconVert)
+ symbolTextAndIcon: compile(symbolTextAndIconFrag, symbolTextAndIconVert),
+ terrain: compile(terrainFrag, terrainVert),
+ terrainDepth: compile(terrainDepthFrag, terrainVert),
+ terrainCoords: compile(terrainCoordsFrag, terrainVert)
};
// Expand #pragmas to #ifdefs.
diff --git a/src/shaders/symbol_icon.vertex.glsl b/src/shaders/symbol_icon.vertex.glsl
index 16ab111429c..964cebc3dda 100644
--- a/src/shaders/symbol_icon.vertex.glsl
+++ b/src/shaders/symbol_icon.vertex.glsl
@@ -15,14 +15,11 @@ uniform highp float u_pitch;
uniform bool u_rotate_symbol;
uniform highp float u_aspect_ratio;
uniform float u_fade_change;
-
uniform mat4 u_matrix;
uniform mat4 u_label_plane_matrix;
uniform mat4 u_coord_matrix;
-
uniform bool u_is_text;
uniform bool u_pitch_with_map;
-
uniform vec2 u_texsize;
varying vec2 v_tex;
@@ -43,6 +40,7 @@ void main() {
vec2 a_pxoffset = a_pixeloffset.xy;
vec2 a_minFontScale = a_pixeloffset.zw / 256.0;
+ float ele = get_elevation(a_pos);
highp float segment_angle = -a_projected_pos[2];
float size;
@@ -54,7 +52,7 @@ void main() {
size = u_size;
}
- vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1);
+ vec4 projectedPoint = u_matrix * vec4(a_pos, ele, 1);
highp float camera_to_anchor_distance = projectedPoint.w;
// See comments in symbol_sdf.vertex
highp float distance_ratio = u_pitch_with_map ?
@@ -72,7 +70,7 @@ void main() {
highp float symbol_rotation = 0.0;
if (u_rotate_symbol) {
// See comments in symbol_sdf.vertex
- vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), 0, 1);
+ vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), ele, 1);
vec2 a = projectedPoint.xy / projectedPoint.w;
vec2 b = offsetProjectedPoint.xy / offsetProjectedPoint.w;
@@ -84,11 +82,13 @@ void main() {
highp float angle_cos = cos(segment_angle + symbol_rotation);
mat2 rotation_matrix = mat2(angle_cos, -1.0 * angle_sin, angle_sin, angle_cos);
- vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0);
- gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * max(a_minFontScale, fontScale) + a_pxoffset / 16.0), 0.0, 1.0);
+ vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, ele, 1.0);
+ float z = float(u_pitch_with_map) * projected_pos.z / projected_pos.w;
+ gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * max(a_minFontScale, fontScale) + a_pxoffset / 16.0), z, 1.0);
v_tex = a_tex / u_texsize;
vec2 fade_opacity = unpack_opacity(a_fade_opacity);
float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change;
- v_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change));
+ float visibility = calculate_visibility(projectedPoint);
+ v_fade_opacity = max(0.0, min(visibility, fade_opacity[0] + fade_change));
}
diff --git a/src/shaders/symbol_sdf.vertex.glsl b/src/shaders/symbol_sdf.vertex.glsl
index 71ccf3c81d7..3776cb6a3dc 100644
--- a/src/shaders/symbol_sdf.vertex.glsl
+++ b/src/shaders/symbol_sdf.vertex.glsl
@@ -54,6 +54,7 @@ void main() {
float a_size_min = floor(a_size[0] * 0.5);
vec2 a_pxoffset = a_pixeloffset.xy;
+ float ele = get_elevation(a_pos);
highp float segment_angle = -a_projected_pos[2];
float size;
@@ -65,7 +66,7 @@ void main() {
size = u_size;
}
- vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1);
+ vec4 projectedPoint = u_matrix * vec4(a_pos, ele, 1);
highp float camera_to_anchor_distance = projectedPoint.w;
// If the label is pitched with the map, layout is done in pitched space,
// which makes labels in the distance smaller relative to viewport space.
@@ -90,7 +91,7 @@ void main() {
// Point labels with 'rotation-alignment: map' are horizontal with respect to tile units
// To figure out that angle in projected space, we draw a short horizontal line in tile
// space, project it, and measure its angle in projected space.
- vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), 0, 1);
+ vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), ele, 1);
vec2 a = projectedPoint.xy / projectedPoint.w;
vec2 b = offsetProjectedPoint.xy / offsetProjectedPoint.w;
@@ -102,13 +103,15 @@ void main() {
highp float angle_cos = cos(segment_angle + symbol_rotation);
mat2 rotation_matrix = mat2(angle_cos, -1.0 * angle_sin, angle_sin, angle_cos);
- vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0);
- gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale + a_pxoffset), 0.0, 1.0);
+ vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, ele, 1.0);
+ float z = float(u_pitch_with_map) * projected_pos.z / projected_pos.w;
+ gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale + a_pxoffset), z, 1.0);
float gamma_scale = gl_Position.w;
vec2 fade_opacity = unpack_opacity(a_fade_opacity);
+ float visibility = calculate_visibility(projectedPoint);
float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change;
- float interpolated_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change));
+ float interpolated_fade_opacity = max(0.0, min(visibility, fade_opacity[0] + fade_change));
v_data0 = a_tex / u_texsize;
v_data1 = vec3(gamma_scale, size, interpolated_fade_opacity);
diff --git a/src/shaders/symbol_text_and_icon.vertex.glsl b/src/shaders/symbol_text_and_icon.vertex.glsl
index 647310fc9c9..4d85246acc8 100644
--- a/src/shaders/symbol_text_and_icon.vertex.glsl
+++ b/src/shaders/symbol_text_and_icon.vertex.glsl
@@ -54,6 +54,7 @@ void main() {
float a_size_min = floor(a_size[0] * 0.5);
float is_sdf = a_size[0] - 2.0 * a_size_min;
+ float ele = get_elevation(a_pos);
highp float segment_angle = -a_projected_pos[2];
float size;
@@ -65,7 +66,7 @@ void main() {
size = u_size;
}
- vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1);
+ vec4 projectedPoint = u_matrix * vec4(a_pos, ele, 1);
highp float camera_to_anchor_distance = projectedPoint.w;
// If the label is pitched with the map, layout is done in pitched space,
// which makes labels in the distance smaller relative to viewport space.
@@ -90,7 +91,7 @@ void main() {
// Point labels with 'rotation-alignment: map' are horizontal with respect to tile units
// To figure out that angle in projected space, we draw a short horizontal line in tile
// space, project it, and measure its angle in projected space.
- vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), 0, 1);
+ vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), ele, 1);
vec2 a = projectedPoint.xy / projectedPoint.w;
vec2 b = offsetProjectedPoint.xy / offsetProjectedPoint.w;
@@ -102,13 +103,15 @@ void main() {
highp float angle_cos = cos(segment_angle + symbol_rotation);
mat2 rotation_matrix = mat2(angle_cos, -1.0 * angle_sin, angle_sin, angle_cos);
- vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0);
- gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale), 0.0, 1.0);
+ vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, ele, 1.0);
+ float z = float(u_pitch_with_map) * projected_pos.z / projected_pos.w;
+ gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale), z, 1.0);
float gamma_scale = gl_Position.w;
vec2 fade_opacity = unpack_opacity(a_fade_opacity);
+ float visibility = calculate_visibility(projectedPoint);
float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change;
- float interpolated_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change));
+ float interpolated_fade_opacity = max(0.0, min(visibility, fade_opacity[0] + fade_change));
v_data0.xy = a_tex / u_texsize;
v_data0.zw = a_tex / u_texsize_icon;
diff --git a/src/shaders/terrain.fragment.glsl b/src/shaders/terrain.fragment.glsl
new file mode 100644
index 00000000000..14e2517dab8
--- /dev/null
+++ b/src/shaders/terrain.fragment.glsl
@@ -0,0 +1,7 @@
+uniform sampler2D u_texture;
+
+varying vec2 v_texture_pos;
+
+void main() {
+ gl_FragColor = texture2D(u_texture, v_texture_pos);
+}
diff --git a/src/shaders/terrain.vertex.glsl b/src/shaders/terrain.vertex.glsl
new file mode 100644
index 00000000000..05b4d378d28
--- /dev/null
+++ b/src/shaders/terrain.vertex.glsl
@@ -0,0 +1,12 @@
+attribute vec2 a_pos;
+
+uniform mat4 u_matrix;
+
+varying vec2 v_texture_pos;
+varying float v_depth;
+
+void main() {
+ v_texture_pos = a_pos / 8192.0; // 8192.0 is the hardcoded vector-tiles coordinates resolution
+ gl_Position = u_matrix * vec4(a_pos, get_elevation(a_pos), 1.0);
+ v_depth = gl_Position.z / gl_Position.w;
+}
diff --git a/src/shaders/terrain_coords.fragment.glsl b/src/shaders/terrain_coords.fragment.glsl
new file mode 100644
index 00000000000..c4d8c7815be
--- /dev/null
+++ b/src/shaders/terrain_coords.fragment.glsl
@@ -0,0 +1,11 @@
+precision mediump float;
+
+uniform sampler2D u_texture;
+uniform float u_terrain_coords_id;
+
+varying vec2 v_texture_pos;
+
+void main() {
+ vec4 rgba = texture2D(u_texture, v_texture_pos);
+ gl_FragColor = vec4(rgba.r, rgba.g, rgba.b, u_terrain_coords_id);
+}
diff --git a/src/shaders/terrain_depth.fragment.glsl b/src/shaders/terrain_depth.fragment.glsl
new file mode 100644
index 00000000000..ff5cf76d8c1
--- /dev/null
+++ b/src/shaders/terrain_depth.fragment.glsl
@@ -0,0 +1,15 @@
+varying float v_depth;
+
+// methods for pack/unpack depth value to texture rgba
+// https://stackoverflow.com/questions/34963366/encode-floating-point-data-in-a-rgba-texture
+const highp vec4 bitSh = vec4(256. * 256. * 256., 256. * 256., 256., 1.);
+const highp vec4 bitMsk = vec4(0.,vec3(1./256.0));
+highp vec4 pack(highp float value) {
+ highp vec4 comp = fract(value * bitSh);
+ comp -= comp.xxyz * bitMsk;
+ return comp;
+}
+
+void main() {
+ gl_FragColor = pack(v_depth);
+}
diff --git a/src/source/raster_dem_tile_source.ts b/src/source/raster_dem_tile_source.ts
index 20b46215155..21825540370 100644
--- a/src/source/raster_dem_tile_source.ts
+++ b/src/source/raster_dem_tile_source.ts
@@ -70,15 +70,16 @@ class RasterDEMTileSource extends RasterTileSource implements Source {
}
}
- function done(err, dem) {
+ function done(err, data) {
if (err) {
tile.state = 'errored';
callback(err);
}
- if (dem) {
- tile.dem = dem;
+ if (data) {
+ tile.dem = data;
tile.needsHillshadePrepare = true;
+ tile.needsTerrainPrepare = true;
tile.state = 'loaded';
callback(null);
}
diff --git a/src/source/raster_dem_tile_worker_source.ts b/src/source/raster_dem_tile_worker_source.ts
index 602a62dd769..f08fb1780cd 100644
--- a/src/source/raster_dem_tile_worker_source.ts
+++ b/src/source/raster_dem_tile_worker_source.ts
@@ -1,6 +1,5 @@
import DEMData from '../data/dem_data';
import {RGBAImage} from '../util/image';
-
import type Actor from '../util/actor';
import type {
WorkerDEMTileParameters,
diff --git a/src/source/source_cache.test.ts b/src/source/source_cache.test.ts
index 4d8d309c5b0..18a9351c117 100644
--- a/src/source/source_cache.test.ts
+++ b/src/source/source_cache.test.ts
@@ -1690,3 +1690,86 @@ describe('SourceCache#onRemove', () => {
expect(sourceOnRemove).toHaveBeenCalled();
});
});
+
+describe('SourceCache#usedForTerrain', () => {
+ test('loads covering tiles with usedForTerrain with source zoom 0-14', done => {
+ const transform = new Transform();
+ transform.resize(511, 511);
+ transform.zoom = 10;
+
+ const sourceCache = createSourceCache({});
+ sourceCache.usedForTerrain = true;
+ sourceCache.tileSize = 1024;
+ expect(sourceCache.usedForTerrain).toBeTruthy();
+ sourceCache.on('data', (e) => {
+ if (e.sourceDataType === 'metadata') {
+ sourceCache.update(transform);
+ expect(Object.values(sourceCache._tiles).map(t => t.tileID.key)).toEqual(
+ ['2tc099', '2tbz99', '2sxs99', '2sxr99', 'pds88', 'eo55', 'pdr88', 'en55', 'p6o88', 'ds55', 'p6n88', 'dr55']
+ );
+ done();
+ }
+ });
+ sourceCache.onAdd(undefined);
+ });
+
+ test('loads covering tiles with usedForTerrain with source zoom 8-14', done => {
+ const transform = new Transform();
+ transform.resize(511, 511);
+ transform.zoom = 10;
+
+ const sourceCache = createSourceCache({minzoom: 8, maxzoom: 14});
+ sourceCache.usedForTerrain = true;
+ sourceCache.tileSize = 1024;
+ sourceCache.on('data', (e) => {
+ if (e.sourceDataType === 'metadata') {
+ sourceCache.update(transform);
+ expect(Object.values(sourceCache._tiles).map(t => t.tileID.key)).toEqual(
+ ['2tc099', '2tbz99', '2sxs99', '2sxr99', 'pds88', 'pdr88', 'p6o88', 'p6n88']
+ );
+ done();
+ }
+ });
+ sourceCache.onAdd(undefined);
+ });
+
+ test('loads covering tiles with usedForTerrain with source zoom 0-4', done => {
+ const transform = new Transform();
+ transform.resize(511, 511);
+ transform.zoom = 10;
+
+ const sourceCache = createSourceCache({minzoom: 0, maxzoom: 4});
+ sourceCache.usedForTerrain = true;
+ sourceCache.tileSize = 1024;
+ sourceCache.on('data', (e) => {
+ if (e.sourceDataType === 'metadata') {
+ sourceCache.update(transform);
+ expect(Object.values(sourceCache._tiles).map(t => t.tileID.key)).toEqual(
+ ['1033', '3s44', '3r44', '3c44', '3b44', 'z33', 's33', 'r33']
+ );
+ done();
+ }
+ });
+ sourceCache.onAdd(undefined);
+ });
+
+ test('loads covering tiles with usedForTerrain with source zoom 4-4', done => {
+ const transform = new Transform();
+ transform.resize(511, 511);
+ transform.zoom = 10;
+
+ const sourceCache = createSourceCache({minzoom: 4, maxzoom: 4});
+ sourceCache.usedForTerrain = true;
+ sourceCache.tileSize = 1024;
+ sourceCache.on('data', (e) => {
+ if (e.sourceDataType === 'metadata') {
+ sourceCache.update(transform);
+ expect(Object.values(sourceCache._tiles).map(t => t.tileID.key)).toEqual(
+ ['3s44', '3r44', '3c44', '3b44']
+ );
+ done();
+ }
+ });
+ sourceCache.onAdd(undefined);
+ });
+});
diff --git a/src/source/source_cache.ts b/src/source/source_cache.ts
index bd70631d87f..ee5b41007bd 100644
--- a/src/source/source_cache.ts
+++ b/src/source/source_cache.ts
@@ -21,6 +21,8 @@ import type Transform from '../geo/transform';
import type {TileState} from './tile';
import type {Callback} from '../types/callback';
import type {SourceSpecification} from '../style-spec/types.g';
+import type {MapSourceDataEvent} from '../ui/events';
+import Terrain from '../render/terrain';
/**
* `SourceCache` is responsible for
@@ -56,7 +58,10 @@ class SourceCache extends Evented {
_shouldReloadOnResume: boolean;
_coveredTiles: {[_: string]: boolean};
transform: Transform;
+ terrain: Terrain;
used: boolean;
+ usedForTerrain: boolean;
+ tileSize: number;
_state: SourceFeatureState;
_loadedParentTiles: {[_: string]: Tile};
@@ -68,7 +73,7 @@ class SourceCache extends Evented {
this.id = id;
this.dispatcher = dispatcher;
- this.on('data', (e) => {
+ this.on('data', (e: MapSourceDataEvent) => {
// this._sourceLoaded signifies that the TileJSON is loaded if applicable.
// if the source type does not come with a TileJSON, the flag signifies the
// source data has loaded (i.e geojson has been tiled on the worker and is ready)
@@ -79,7 +84,7 @@ class SourceCache extends Evented {
if (this._sourceLoaded && !this._paused && e.dataType === 'source' && e.sourceDataType === 'content') {
this.reload();
if (this.transform) {
- this.update(this.transform);
+ this.update(this.transform, this.terrain);
}
}
});
@@ -152,7 +157,7 @@ class SourceCache extends Evented {
this._paused = false;
this._shouldReloadOnResume = false;
if (shouldReload) this.reload();
- if (this.transform) this.update(this.transform);
+ if (this.transform) this.update(this.transform, this.terrain);
}
_loadTile(tile: Tile, callback: Callback) {
@@ -263,7 +268,7 @@ class SourceCache extends Evented {
tile.state = 'errored';
if ((err as any).status !== 404) this._source.fire(new ErrorEvent(err, {tile}));
// continue to try loading parent/children tiles if a tile doesn't exist (404)
- else this.update(this.transform);
+ else this.update(this.transform, this.terrain);
return;
}
@@ -295,6 +300,7 @@ class SourceCache extends Evented {
function fillBorder(tile, borderTile) {
tile.needsHillshadePrepare = true;
+ tile.needsTerrainPrepare = true;
let dx = borderTile.tileID.canonical.x - tile.tileID.canonical.x;
const dy = borderTile.tileID.canonical.y - tile.tileID.canonical.y;
const dim = Math.pow(2, tile.tileID.canonical.z);
@@ -486,8 +492,9 @@ class SourceCache extends Evented {
* are inside the viewport.
* @private
*/
- update(transform: Transform) {
+ update(transform: Transform, terrain: Terrain) {
this.transform = transform;
+ this.terrain = terrain;
if (!this._sourceLoaded || this._paused) { return; }
this.updateCacheSize(transform);
@@ -498,18 +505,19 @@ class SourceCache extends Evented {
this._coveredTiles = {};
let idealTileIDs;
- if (!this.used) {
+ if (!this.used && !this.usedForTerrain) {
idealTileIDs = [];
} else if (this._source.tileID) {
idealTileIDs = transform.getVisibleUnwrappedCoordinates(this._source.tileID)
.map((unwrapped) => new OverscaledTileID(unwrapped.canonical.z, unwrapped.wrap, unwrapped.canonical.z, unwrapped.canonical.x, unwrapped.canonical.y));
} else {
idealTileIDs = transform.coveringTiles({
- tileSize: this._source.tileSize,
+ tileSize: this.usedForTerrain ? this.tileSize : this._source.tileSize,
minzoom: this._source.minzoom,
maxzoom: this._source.maxzoom,
- roundZoom: this._source.roundZoom,
- reparseOverscaled: this._source.reparseOverscaled
+ roundZoom: this.usedForTerrain ? false : this._source.roundZoom,
+ reparseOverscaled: this._source.reparseOverscaled,
+ terrain
});
if (this._source.hasTile) {
@@ -522,6 +530,21 @@ class SourceCache extends Evented {
const minCoveringZoom = Math.max(zoom - SourceCache.maxOverzooming, this._source.minzoom);
const maxCoveringZoom = Math.max(zoom + SourceCache.maxUnderzooming, this._source.minzoom);
+ // When sourcecache is used for terrain also load parent tiles to avoid flickering when zooming out
+ if (this.usedForTerrain) {
+ const parents = {};
+ for (const tileID of idealTileIDs) {
+ if (tileID.canonical.z > this._source.minzoom) {
+ const parent = tileID.scaledTo(tileID.canonical.z - 1);
+ parents[parent.key] = parent;
+ // load very low zoom to calculate tile visability in transform.coveringTiles and high zoomlevels correct
+ const parent2 = tileID.scaledTo(Math.max(this._source.minzoom, Math.min(tileID.canonical.z, 5)));
+ parents[parent2.key] = parent2;
+ }
+ }
+ idealTileIDs = idealTileIDs.concat(Object.values(parents));
+ }
+
// Retain is a list of tiles that we shouldn't delete, even if they are not
// the most ideal tile for the current viewport. This may include tiles like
// parent or child tiles that are *already* loaded.
@@ -558,6 +581,44 @@ class SourceCache extends Evented {
retain[id] = parentsForFading[id];
}
}
+
+ // disable fading logic in terrain3D mode to avoid rendering two tiles on the same place
+ if (terrain) {
+ const idealRasterTileIDs: {[_: string]: OverscaledTileID} = {};
+ const missingTileIDs: {[_: string]: OverscaledTileID} = {};
+ for (const tileID of idealTileIDs) {
+ if (this._tiles[tileID.key].hasData())
+ idealRasterTileIDs[tileID.key] = tileID;
+ else
+ missingTileIDs[tileID.key] = tileID;
+ }
+ // search for a complete set of children for each missing tile
+ for (const key in missingTileIDs) {
+ const children = missingTileIDs[key].children(this._source.maxzoom);
+ if (this._tiles[children[0].key] && this._tiles[children[1].key] && this._tiles[children[2].key] && this._tiles[children[3].key]) {
+ idealRasterTileIDs[children[0].key] = retain[children[0].key] = children[0];
+ idealRasterTileIDs[children[1].key] = retain[children[1].key] = children[1];
+ idealRasterTileIDs[children[2].key] = retain[children[2].key] = children[2];
+ idealRasterTileIDs[children[3].key] = retain[children[3].key] = children[3];
+ delete missingTileIDs[key];
+ }
+ }
+ // search for parent for each missing tile
+ for (const key in missingTileIDs) {
+ const parent = this.findLoadedParent(missingTileIDs[key], this._source.minzoom);
+ if (parent) {
+ idealRasterTileIDs[parent.tileID.key] = retain[parent.tileID.key] = parent.tileID;
+ // remove idealTiles which would be rendered twice
+ for (const key in idealRasterTileIDs) {
+ if (idealRasterTileIDs[key].isChildOf(parent.tileID)) delete idealRasterTileIDs[key];
+ }
+ }
+ }
+ // cover all tiles which are not needed
+ for (const key in this._tiles) {
+ if (!idealRasterTileIDs[key]) this._coveredTiles[key] = true;
+ }
+ }
}
for (const retainedId in retain) {
@@ -819,8 +880,8 @@ class SourceCache extends Evented {
transform.getCameraQueryGeometry(pointQueryGeometry) :
pointQueryGeometry;
- const queryGeometry = pointQueryGeometry.map((p) => transform.pointCoordinate(p));
- const cameraQueryGeometry = cameraPointQueryGeometry.map((p) => transform.pointCoordinate(p));
+ const queryGeometry = pointQueryGeometry.map((p: Point) => transform.pointCoordinate(p, this.terrain));
+ const cameraQueryGeometry = cameraPointQueryGeometry.map((p: Point) => transform.pointCoordinate(p, this.terrain));
const ids = this.getIds();
diff --git a/src/source/terrain_source_cache.test.ts b/src/source/terrain_source_cache.test.ts
new file mode 100644
index 00000000000..053d30c5e4c
--- /dev/null
+++ b/src/source/terrain_source_cache.test.ts
@@ -0,0 +1,89 @@
+import TerrainSourceCache from './terrain_source_cache';
+import Style from '../style/style';
+import {RequestManager} from '../util/request_manager';
+import Dispatcher from '../util/dispatcher';
+import {fakeServer, FakeServer} from 'nise';
+import Transform from '../geo/transform';
+import {Evented} from '../util/evented';
+import Painter from '../render/painter';
+import Context from '../gl/context';
+import gl from 'gl';
+import RasterDEMTileSource from './raster_dem_tile_source';
+
+const context = new Context(gl(10, 10));
+const transform = new Transform();
+
+class StubMap extends Evented {
+ transform: Transform;
+ painter: Painter;
+ _requestManager: RequestManager;
+
+ constructor() {
+ super();
+ this.transform = transform;
+ this._requestManager = {
+ transformRequest: (url) => {
+ return {url};
+ }
+ } as any as RequestManager;
+ }
+}
+
+function createSource(options, transformCallback?) {
+ const source = new RasterDEMTileSource('id', options, {send() {}} as any as Dispatcher, null);
+ source.onAdd({
+ transform,
+ _getMapId: () => 1,
+ _requestManager: new RequestManager(transformCallback),
+ getPixelRatio() { return 1; }
+ } as any);
+
+ source.on('error', (e) => {
+ throw e.error;
+ });
+
+ return source;
+}
+
+describe('TerrainSourceCache', () => {
+ let server: FakeServer;
+ let style: Style;
+ let tsc: TerrainSourceCache;
+
+ beforeAll(done => {
+ global.fetch = null;
+ server = fakeServer.create();
+ server.respondWith('/source.json', JSON.stringify({
+ minzoom: 0,
+ maxzoom: 22,
+ attribution: 'MapLibre',
+ tiles: ['http://example.com/{z}/{x}/{y}.pngraw'],
+ bounds: [-47, -7, -45, -5]
+ }));
+ const map = new StubMap();
+ style = new Style(map as any);
+ style.map.painter = {style, context} as any;
+ style.on('style.load', () => {
+ const source = createSource({url: '/source.json'});
+ server.respond();
+ style.addSource('terrain', source as any);
+ tsc = new TerrainSourceCache(style.sourceCaches.terrain);
+ done();
+ });
+ style.loadJSON({
+ 'version': 8,
+ 'sources': {},
+ 'layers': []
+ });
+ });
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ test('#constructor', () => {
+ expect(tsc.sourceCache.usedForTerrain).toBeTruthy();
+ expect(tsc.sourceCache.tileSize).toBe(tsc.tileSize * 2 ** tsc.deltaZoom);
+ });
+
+});
diff --git a/src/source/terrain_source_cache.ts b/src/source/terrain_source_cache.ts
new file mode 100644
index 00000000000..6ec9a12407e
--- /dev/null
+++ b/src/source/terrain_source_cache.ts
@@ -0,0 +1,201 @@
+import {OverscaledTileID} from './tile_id';
+import Tile from './tile';
+import EXTENT from '../data/extent';
+import {mat4} from 'gl-matrix';
+import {Evented} from '../util/evented';
+import type Transform from '../geo/transform';
+import type SourceCache from '../source/source_cache';
+import Painter from '../render/painter';
+import Terrain from '../render/terrain';
+
+/**
+ * This class is a helper for the Terrain-class, it:
+ * - loads raster-dem tiles
+ * - manages all renderToTexture tiles.
+ * - caches previous rendered tiles.
+ * - finds all necessary renderToTexture tiles for a OverscaledTileID area
+ * - finds the corresponding raster-dem tile for OverscaledTileID
+ */
+
+export default class TerrainSourceCache extends Evented {
+ // source-cache for the raster-dem source.
+ sourceCache: SourceCache;
+ // stores all render-to-texture tiles.
+ _tiles: {[_: string]: Tile};
+ // contains a list of tileID-keys for the current scene. (only for performance)
+ _renderableTilesKeys: Array;
+ // raster-dem-tile for a TileID cache.
+ _sourceTileCache: {[_: string]: string};
+ // minimum zoomlevel to render the terrain.
+ minzoom: number;
+ // maximum zoomlevel to render the terrain.
+ maxzoom: number;
+ // render-to-texture tileSize in scene.
+ tileSize: number;
+ // raster-dem tiles will load for performance the actualZoom - deltaZoom zoom-level.
+ deltaZoom: number;
+ // each time a render-to-texture tile is rendered, its tileID.key is stored into this array
+ renderHistory: Array;
+ // maximal size of render-history
+ renderHistorySize: number;
+
+ constructor(sourceCache: SourceCache) {
+ super();
+ this.sourceCache = sourceCache;
+ this._tiles = {};
+ this._renderableTilesKeys = [];
+ this._sourceTileCache = {};
+ this.renderHistory = [];
+ this.minzoom = 0;
+ this.maxzoom = 22;
+ this.tileSize = 512;
+ this.deltaZoom = 1;
+ this.renderHistorySize = 150;
+ sourceCache.usedForTerrain = true;
+ sourceCache.tileSize = this.tileSize * 2 ** this.deltaZoom;
+ }
+
+ destruct() {
+ this.sourceCache.usedForTerrain = false;
+ this.sourceCache.tileSize = null;
+ for (const key in this._tiles) {
+ const tile = this._tiles[key];
+ tile.textures.forEach(t => t.destroy());
+ tile.textures = [];
+ }
+ }
+
+ /**
+ * Load Terrain Tiles, create internal render-to-texture tiles, free GPU memory.
+ * @param {Transform} transform - the operation to do
+ * @param {Terrain} terrain - the terrain
+ */
+ update(transform: Transform, terrain: Terrain): void {
+ // load raster-dem tiles for the current scene.
+ this.sourceCache.update(transform, terrain);
+ // create internal render-to-texture tiles for the current scene.
+ this._renderableTilesKeys = [];
+ for (const tileID of transform.coveringTiles({
+ tileSize: this.tileSize,
+ minzoom: this.minzoom,
+ maxzoom: this.maxzoom,
+ reparseOverscaled: false,
+ terrain
+ })) {
+ this._renderableTilesKeys.push(tileID.key);
+ if (!this._tiles[tileID.key]) {
+ tileID.posMatrix = new Float64Array(16) as any;
+ mat4.ortho(tileID.posMatrix, 0, EXTENT, 0, EXTENT, 0, 1);
+ this._tiles[tileID.key] = new Tile(tileID, this.tileSize);
+ }
+ }
+ }
+
+ /**
+ * This method should called before each render-to-texture step to free old cached tiles
+ * @param {Painter} painter - the painter
+ */
+ removeOutdated(painter: Painter) {
+ // create lookuptable for actual needed tiles
+ const tileIDs = {};
+ for (const key of this._renderableTilesKeys) tileIDs[key] = true;
+ // remove duplicates from renderHistory
+ this.renderHistory = this.renderHistory.filter((i, p) => this.renderHistory.indexOf(i) === p);
+ // free (GPU) memory from previously rendered not needed tiles
+ while (this.renderHistory.length > this.renderHistorySize) {
+ const tile = this.sourceCache._tiles[this.renderHistory.shift()];
+ if (tile && !tileIDs[tile.tileID.key]) {
+ tile.clearTextures(painter);
+ delete this.sourceCache._tiles[tile.tileID.key];
+ }
+ }
+ }
+
+ /**
+ * get a list of tiles, which are loaded and should be rendered in the current scene
+ * @returns {Array} the renderable tiles
+ */
+ getRenderableTiles(): Array {
+ return this._renderableTilesKeys.map(key => this.getTileByID(key));
+ }
+
+ /**
+ * get terrain tile by the TileID key
+ * @param id - the tile id
+ * @returns {Tile} - the tile
+ */
+ getTileByID(id: string): Tile {
+ return this._tiles[id];
+ }
+
+ /**
+ * searches for the corresponding current renderable terrain-tiles
+ * @param {OverscaledTileID} tileID - the tile to look for
+ * @returns {[_:string]: Tile} - the tiles that were found
+ */
+ getTerrainCoords(tileID: OverscaledTileID): {[_: string]: OverscaledTileID} {
+ const coords = {};
+ for (const key of this._renderableTilesKeys) {
+ const _tileID = this._tiles[key].tileID;
+ if (_tileID.canonical.equals(tileID.canonical)) {
+ const coord = tileID.clone();
+ coord.posMatrix = new Float64Array(16) as any;
+ mat4.ortho(coord.posMatrix, 0, EXTENT, 0, EXTENT, 0, 1);
+ coords[key] = coord;
+ } else if (_tileID.canonical.isChildOf(tileID.canonical)) {
+ const coord = tileID.clone();
+ coord.posMatrix = new Float64Array(16) as any;
+ const dz = _tileID.canonical.z - tileID.canonical.z;
+ const dx = _tileID.canonical.x - (_tileID.canonical.x >> dz << dz);
+ const dy = _tileID.canonical.y - (_tileID.canonical.y >> dz << dz);
+ const size = EXTENT >> dz;
+ mat4.ortho(coord.posMatrix, 0, size, 0, size, 0, 1);
+ mat4.translate(coord.posMatrix, coord.posMatrix, [-dx * size, -dy * size, 0]);
+ coords[key] = coord;
+ } else if (tileID.canonical.isChildOf(_tileID.canonical)) {
+ const coord = tileID.clone();
+ coord.posMatrix = new Float64Array(16) as any;
+ const dz = tileID.canonical.z - _tileID.canonical.z;
+ const dx = tileID.canonical.x - (tileID.canonical.x >> dz << dz);
+ const dy = tileID.canonical.y - (tileID.canonical.y >> dz << dz);
+ const size = EXTENT >> dz;
+ mat4.ortho(coord.posMatrix, 0, EXTENT, 0, EXTENT, 0, 1);
+ mat4.translate(coord.posMatrix, coord.posMatrix, [dx * size, dy * size, 0]);
+ mat4.scale(coord.posMatrix, coord.posMatrix, [1 / (2 ** dz), 1 / (2 ** dz), 0]);
+ coords[key] = coord;
+ }
+ }
+ return coords;
+ }
+
+ /**
+ * find the covering raster-dem tile
+ * @param {OverscaledTileID} tileID - the tile to look for
+ * @param {boolean} searchForDEM Optinal parameter to search for (parent) souretiles with loaded dem.
+ * @returns {Tile} - the tile
+ */
+ getSourceTile(tileID: OverscaledTileID, searchForDEM?: boolean): Tile {
+ const source = this.sourceCache._source;
+ let z = tileID.overscaledZ - this.deltaZoom;
+ if (z > source.maxzoom) z = source.maxzoom;
+ if (z < source.minzoom) return null;
+ // cache for tileID to terrain-tileID
+ if (!this._sourceTileCache[tileID.key])
+ this._sourceTileCache[tileID.key] = tileID.scaledTo(z).key;
+ let tile = this.sourceCache.getTileByID(this._sourceTileCache[tileID.key]);
+ // during tile-loading phase look if parent tiles (with loaded dem) are available.
+ if (!(tile && tile.dem) && searchForDEM)
+ while (z > source.minzoom && !(tile && tile.dem))
+ tile = this.sourceCache.getTileByID(tileID.scaledTo(z--).key);
+ return tile;
+ }
+
+ /**
+ * get a list of tiles, loaded after a spezific time. This is used to update depth & coords framebuffers.
+ * @param {Date} time - the time
+ * @returns {Array} - the relevant tiles
+ */
+ tilesAfterTime(time = Date.now()): Array {
+ return Object.values(this._tiles).filter(t => t.timeLoaded >= time);
+ }
+}
diff --git a/src/source/tile.ts b/src/source/tile.ts
index 8f234d2e91e..23e0fb58be5 100644
--- a/src/source/tile.ts
+++ b/src/source/tile.ts
@@ -65,6 +65,7 @@ class Tile {
expiredRequestCount: number;
state: TileState;
timeAdded: any;
+ timeLoaded: any;
fadeEndTime: any;
collisionBoxArray: CollisionBoxArray;
redoWhenDone: boolean;
@@ -75,8 +76,10 @@ class Tile {
neighboringTiles: any;
dem: DEMData;
+ demMatrix: mat4;
aborted: boolean;
needsHillshadePrepare: boolean;
+ needsTerrainPrepare: boolean;
request: Cancelable;
texture: any;
fbo: Framebuffer;
@@ -90,6 +93,8 @@ class Tile {
hasSymbolBuckets: boolean;
hasRTLText: boolean;
dependencies: any;
+ textures: Array;
+ textureCoords: {[_: string]: string}; // remeber all coords rendered to textures
/**
* @param {OverscaledTileID} tileID
@@ -107,6 +112,8 @@ class Tile {
this.hasSymbolBuckets = false;
this.hasRTLText = false;
this.dependencies = {};
+ this.textures = [];
+ this.textureCoords = {};
// Counts the number of times a response was already expired when
// received. We're using this to add a delay when making a new request
@@ -129,6 +136,14 @@ class Tile {
return this.state === 'errored' || this.state === 'loaded' || this.state === 'reloading';
}
+ clearTextures(painter: any) {
+ if (this.demTexture) painter.saveTileTexture(this.demTexture);
+ this.textures.forEach(t => painter.saveTileTexture(t));
+ this.demTexture = null;
+ this.textures = [];
+ this.textureCoords = {};
+ }
+
/**
* Given a data object with a 'buffers' property, load it into
* this tile's elementGroups and buffers properties and set loaded
diff --git a/src/source/tile_id.ts b/src/source/tile_id.ts
index 294c17617c1..4600733e4cb 100644
--- a/src/source/tile_id.ts
+++ b/src/source/tile_id.ts
@@ -42,6 +42,11 @@ export class CanonicalTileID {
.replace(/{bbox-epsg-3857}/g, bbox);
}
+ isChildOf(parent: CanonicalTileID) {
+ const dz = this.z - parent.z;
+ return dz > 0 && parent.x === (this.x >> dz) && parent.y === (this.y >> dz);
+ }
+
getTilePoint(coord: MercatorCoordinate) {
const tilesAtZoom = Math.pow(2, this.z);
return new Point(
@@ -81,6 +86,10 @@ export class OverscaledTileID {
this.key = calculateKey(wrap, overscaledZ, z, x, y);
}
+ clone() {
+ return new OverscaledTileID(this.overscaledZ, this.wrap, this.canonical.z, this.canonical.x, this.canonical.y);
+ }
+
equals(id: OverscaledTileID) {
return this.overscaledZ === id.overscaledZ && this.wrap === id.wrap && this.canonical.equals(id.canonical);
}
diff --git a/src/style-spec/error/validation_error.ts b/src/style-spec/error/validation_error.ts
index 3b7cdf7be88..0c489cc396f 100644
--- a/src/style-spec/error/validation_error.ts
+++ b/src/style-spec/error/validation_error.ts
@@ -5,7 +5,7 @@ export default class ValidationError {
identifier: string;
line: number;
- constructor(key: string, value: {
+ constructor(key: string, value: any & {
__line__: number;
}, message: string, identifier?: string | null) {
this.message = (key ? `${key}: ` : '') + message;
diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json
index b8e070a2d90..e4a7df58313 100644
--- a/src/style-spec/reference/v8.json
+++ b/src/style-spec/reference/v8.json
@@ -57,6 +57,15 @@
"intensity": 0.4
}
},
+ "terrain": {
+ "type": "terrain",
+ "doc": "The terrain configuration.",
+ "example": {
+ "source": "raster-dem-source",
+ "exaggeration": 0.5,
+ "elevationOffset": 100
+ }
+ },
"sources": {
"required": true,
"type": "sources",
@@ -3810,6 +3819,39 @@
}
}
},
+ "terrain": {
+ "source": {
+ "type": "string",
+ "doc": "The source for the terrain data.",
+ "required": true,
+ "sdk-support": {
+ "basic functionality": {
+ "js": "2.2.0"
+ }
+ }
+ },
+ "exaggeration": {
+ "type": "number",
+ "minimum": 0,
+ "doc": "The exaggeration of the terrain - how high it will look.",
+ "default": 1.0,
+ "sdk-support": {
+ "basic functionality": {
+ "js": "2.2.0"
+ }
+ }
+ },
+ "elevationOffset": {
+ "type": "number",
+ "doc": "The elevation offset.",
+ "default": 450,
+ "sdk-support": {
+ "basic functionality": {
+ "js": "2.2.0"
+ }
+ }
+ }
+ },
"paint": [
"paint_fill",
"paint_line",
diff --git a/src/style-spec/validate/validate.ts b/src/style-spec/validate/validate.ts
index 2c2c8aaaa1e..b827db3ba79 100644
--- a/src/style-spec/validate/validate.ts
+++ b/src/style-spec/validate/validate.ts
@@ -17,6 +17,7 @@ import validateFilter from './validate_filter';
import validateLayer from './validate_layer';
import validateSource from './validate_source';
import validateLight from './validate_light';
+import validateTerrain from './validate_terrain';
import validateString from './validate_string';
import validateFormatted from './validate_formatted';
import validateImage from './validate_image';
@@ -37,6 +38,7 @@ const VALIDATORS = {
'object': validateObject,
'source': validateSource,
'light': validateLight,
+ 'terrain': validateTerrain,
'string': validateString,
'formatted': validateFormatted,
'resolvedImage': validateImage
diff --git a/src/style-spec/validate/validate_terrain.test.ts b/src/style-spec/validate/validate_terrain.test.ts
new file mode 100644
index 00000000000..84cc507e1c0
--- /dev/null
+++ b/src/style-spec/validate/validate_terrain.test.ts
@@ -0,0 +1,46 @@
+import validateTerrain from './validate_terrain';
+import v8 from '../reference/v8.json';
+
+describe('Validate Terrain', () => {
+ test('Should return error in case terrain is not an object', () => {
+ const errors = validateTerrain({value: 1 as any, styleSpec: v8, style: {} as any});
+ expect(errors).toHaveLength(1);
+ expect(errors[0].message).toContain('number');
+ expect(errors[0].message).toContain('object');
+ expect(errors[0].message).toContain('terrain');
+ });
+
+ test('Should return error in case terrain source is not a string', () => {
+ const errors = validateTerrain({value: {source: 1 as any}, styleSpec: v8, style: {} as any});
+ expect(errors).toHaveLength(1);
+ expect(errors[0].message).toContain('number');
+ expect(errors[0].message).toContain('string');
+ expect(errors[0].message).toContain('source');
+ });
+
+ test('Should return error in case of unknown property', () => {
+ const errors = validateTerrain({value: {a: 1} as any, styleSpec: v8, style: {} as any});
+ expect(errors).toHaveLength(1);
+ expect(errors[0].message).toContain('a');
+ expect(errors[0].message).toContain('unknown');
+ });
+
+ test('Should return errors according to spec violations', () => {
+ const errors = validateTerrain({value: {source: 1 as any, exaggeration: {} as any, elevationOffset: 'ex2' as any}, styleSpec: v8, style: {} as any});
+ expect(errors).toHaveLength(3);
+ expect(errors[0].message).toContain('number');
+ expect(errors[0].message).toContain('string');
+ expect(errors[0].message).toContain('source');
+ expect(errors[1].message).toContain('number');
+ expect(errors[1].message).toContain('object');
+ expect(errors[1].message).toContain('exaggeration');
+ expect(errors[2].message).toContain('number');
+ expect(errors[2].message).toContain('string');
+ expect(errors[2].message).toContain('elevationOffset');
+ });
+
+ test('Should pass if everything is according to spec', () => {
+ const errors = validateTerrain({value: {source: 'source-id', elevationOffset: 1, exaggeration: 0.2}, styleSpec: v8, style: {} as any});
+ expect(errors).toHaveLength(0);
+ });
+});
diff --git a/src/style-spec/validate/validate_terrain.ts b/src/style-spec/validate/validate_terrain.ts
new file mode 100644
index 00000000000..f75153c87ea
--- /dev/null
+++ b/src/style-spec/validate/validate_terrain.ts
@@ -0,0 +1,41 @@
+import ValidationError from '../error/validation_error';
+import getType from '../util/get_type';
+import validate from './validate';
+import type {StyleSpecification, TerrainSpecification} from '../types.g';
+import type v8 from '../reference/v8.json';
+
+export default function validateTerrain(
+ options: {value: TerrainSpecification; styleSpec: typeof v8; style: StyleSpecification}
+): ValidationError[] {
+
+ const terrain = options.value;
+ const styleSpec = options.styleSpec;
+ const terrainSpec = styleSpec.terrain;
+ const style = options.style;
+
+ let errors = [];
+
+ const rootType = getType(terrain);
+ if (terrain === undefined) {
+ return errors;
+ } else if (rootType !== 'object') {
+ errors = errors.concat([new ValidationError('terrain', terrain, `object expected, ${rootType} found`)]);
+ return errors;
+ }
+
+ for (const key in terrain) {
+ if (terrainSpec[key]) {
+ errors = errors.concat(validate({
+ key,
+ value: terrain[key],
+ valueSpec: terrainSpec[key],
+ style,
+ styleSpec
+ }));
+ } else {
+ errors = errors.concat([new ValidationError(key, terrain[key], `unknown property "${key}"`)]);
+ }
+ }
+
+ return errors;
+}
diff --git a/src/style-spec/validate_style.min.ts b/src/style-spec/validate_style.min.ts
index 79ab747cf4a..816ed2806a0 100644
--- a/src/style-spec/validate_style.min.ts
+++ b/src/style-spec/validate_style.min.ts
@@ -6,6 +6,7 @@ import validateGlyphsURL from './validate/validate_glyphs_url';
import validateSource from './validate/validate_source';
import validateLight from './validate/validate_light';
+import validateTerrain from './validate/validate_terrain';
import validateLayer from './validate/validate_layer';
import validateFilter from './validate/validate_filter';
import validatePaintProperty from './validate/validate_paint_property';
@@ -58,6 +59,7 @@ function validateStyleMin(style, styleSpec = latestStyleSpec) {
validateStyleMin.source = wrapCleanErrors(validateSource);
validateStyleMin.light = wrapCleanErrors(validateLight);
+validateStyleMin.terrain = wrapCleanErrors(validateTerrain);
validateStyleMin.layer = wrapCleanErrors(validateLayer);
validateStyleMin.filter = wrapCleanErrors(validateFilter);
validateStyleMin.paintProperty = wrapCleanErrors(validatePaintProperty);
diff --git a/src/style-spec/validate_style.ts b/src/style-spec/validate_style.ts
index f8f83600892..1e809aa2a3a 100644
--- a/src/style-spec/validate_style.ts
+++ b/src/style-spec/validate_style.ts
@@ -33,6 +33,7 @@ export default function validateStyle(style, styleSpec = v8) {
export const source = validateStyleMin.source;
export const light = validateStyleMin.light;
+export const terrain = validateStyleMin.terrain;
export const layer = validateStyleMin.layer;
export const filter = validateStyleMin.filter;
export const paintProperty = validateStyleMin.paintProperty;
diff --git a/src/style/pauseable_placement.ts b/src/style/pauseable_placement.ts
index 2aeb6e7f805..7fc1b5c738e 100644
--- a/src/style/pauseable_placement.ts
+++ b/src/style/pauseable_placement.ts
@@ -7,6 +7,7 @@ import type StyleLayer from './style_layer';
import type SymbolStyleLayer from './style_layer/symbol_style_layer';
import type Tile from '../source/tile';
import type {BucketPart} from '../symbol/placement';
+import Terrain from '../render/terrain';
class LayerPlacement {
_sortAcrossTiles: boolean;
@@ -69,6 +70,7 @@ class PauseablePlacement {
constructor(
transform: Transform,
+ terrain: Terrain,
order: Array,
forceFullPlacement: boolean,
showCollisionBoxes: boolean,
@@ -76,7 +78,7 @@ class PauseablePlacement {
crossSourceCollisions: boolean,
prevPlacement?: Placement
) {
- this.placement = new Placement(transform, fadeDuration, crossSourceCollisions, prevPlacement);
+ this.placement = new Placement(transform, terrain, fadeDuration, crossSourceCollisions, prevPlacement);
this._currentPlacementIndex = order.length - 1;
this._forceFullPlacement = forceFullPlacement;
this._showCollisionBoxes = showCollisionBoxes;
diff --git a/src/style/style.test.ts b/src/style/style.test.ts
index f12159b02e1..8cb705b17b3 100644
--- a/src/style/style.test.ts
+++ b/src/style/style.test.ts
@@ -379,6 +379,22 @@ describe('Style#loadJSON', () => {
style._layers.background.fire(new Event('error', {mapLibre: true}));
});
});
+
+ test('sets terrain if defined', (done) => {
+ const map = getStubMap();
+ const style = new Style(map);
+ map.transform.updateElevation = jest.fn();
+ style.loadJSON(createStyleJSON({
+ sources: {'source-id': createGeoJSONSource()},
+ terrain: {source: 'source-id', exaggeration: 0.33}
+ }));
+
+ style.on('style.load', () => {
+ expect(style.terrain).toBeDefined();
+ expect(map.transform.updateElevation).toHaveBeenCalled();
+ done();
+ });
+ });
});
describe('Style#_remove', () => {
diff --git a/src/style/style.ts b/src/style/style.ts
index c773438b7d9..f257d0bf40c 100644
--- a/src/style/style.ts
+++ b/src/style/style.ts
@@ -57,11 +57,13 @@ import type {
FilterSpecification,
StyleSpecification,
LightSpecification,
- SourceSpecification
+ SourceSpecification,
+ TerrainSpecification
} from '../style-spec/types.g';
import type {CustomLayerInterface} from './style_layer/custom_style_layer';
import type {Validator} from './validate_style';
import type {OverscaledTileID} from '../source/tile_id';
+import Terrain from '../render/terrain';
const supportedDiffOperations = pick(diffOperations, [
'addLayer',
@@ -102,6 +104,7 @@ export type StyleOptions = {
export type StyleSetterOptions = {
validate?: boolean;
};
+
/**
* @private
*/
@@ -113,6 +116,7 @@ class Style extends Evented {
glyphManager: GlyphManager;
lineAtlas: LineAtlas;
light: Light;
+ terrain: Terrain;
_request: Cancelable;
_spriteRequest: Cancelable;
@@ -123,6 +127,8 @@ class Style extends Evented {
zoomHistory: ZoomHistory;
_loaded: boolean;
_rtlTextPluginCallback: (a: any) => any;
+ _terrainDataCallback: (e: any) => any;
+ _terrainfreezeElevationCallback: (e: any) => any;
_changed: boolean;
_updatedSources: {[_: string]: 'clear' | 'reload'};
_updatedLayers: {[_: string]: true};
@@ -278,6 +284,8 @@ class Style extends Evented {
this.light = new Light(this.stylesheet.light);
+ this.setTerrain(this.stylesheet.terrain);
+
this.fire(new Event('data', {dataType: 'style'}));
this.fire(new Event('style.load'));
}
@@ -478,6 +486,52 @@ class Style extends Evented {
this._changedImages = {};
}
+ /**
+ * Loads a 3D terrain mesh, based on a "raster-dem" source.
+ * @param {TerrainSpecification} [options] Options object.
+ */
+ setTerrain(options?: TerrainSpecification) {
+ this._checkLoaded();
+
+ // clear event handlers
+ if (this._terrainDataCallback) this.off('data', this._terrainDataCallback);
+ if (this._terrainfreezeElevationCallback) this.map.off('freezeElevation', this._terrainfreezeElevationCallback);
+
+ // remove terrain
+ if (!options) {
+ this.terrain = null;
+ this.map.transform.updateElevation(this.terrain);
+
+ // add terrain
+ } else {
+ const sourceCache = this.sourceCaches[options.source];
+ if (!sourceCache) throw new Error(`cannot load terrain, because there exists no source with ID: ${options.source}`);
+ this.terrain = new Terrain(this, sourceCache, options);
+ this.map.transform.updateElevation(this.terrain);
+ this._terrainfreezeElevationCallback = (e: any) => {
+ if (e.freeze) {
+ this.map.transform.freezeElevation = true;
+ } else {
+ this.map.transform.freezeElevation = false;
+ this.map.transform.recalculateZoom(this.terrain);
+ }
+ };
+ this._terrainDataCallback = e => {
+ if (!e.tile) return;
+ if (e.sourceId === options.source) {
+ this.map.transform.updateElevation(this.terrain);
+ this.terrain.rememberForRerender(e.sourceId, e.tile.tileID);
+ } else if (e.source.type === 'geojson') {
+ this.terrain.rememberForRerender(e.sourceId, e.tile.tileID);
+ }
+ };
+ this.on('data', this._terrainDataCallback);
+ this.map.on('freezeElevation', this._terrainfreezeElevationCallback);
+ }
+
+ this.map.fire(new Event('terrain', {terrain: options}));
+ }
+
/**
* Update this style's state to match the given style JSON, performing only
* the necessary mutations.
@@ -1259,7 +1313,7 @@ class Style extends Evented {
_updateSources(transform: Transform) {
for (const id in this.sourceCaches) {
- this.sourceCaches[id].update(transform);
+ this.sourceCaches[id].update(transform, this.terrain);
}
}
@@ -1300,7 +1354,7 @@ class Style extends Evented {
forceFullPlacement = forceFullPlacement || this._layerOrderChanged || fadeDuration === 0;
if (forceFullPlacement || !this.pauseablePlacement || (this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom))) {
- this.pauseablePlacement = new PauseablePlacement(transform, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement);
+ this.pauseablePlacement = new PauseablePlacement(transform, this.terrain, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement);
this._layerOrderChanged = false;
}
diff --git a/src/style/validate_style.ts b/src/style/validate_style.ts
index c257eea3b4b..81efa18fe17 100644
--- a/src/style/validate_style.ts
+++ b/src/style/validate_style.ts
@@ -15,6 +15,7 @@ type ValidateStyle = {
source: Validator;
layer: Validator;
light: Validator;
+ terrain: Validator;
filter: Validator;
paintProperty: Validator;
layoutProperty: Validator;
@@ -25,6 +26,7 @@ export const validateStyle = (validateStyleMin as ValidateStyle);
export const validateSource = validateStyle.source;
export const validateLight = validateStyle.light;
+export const validateTerrain = validateStyle.terrain;
export const validateFilter = validateStyle.filter;
export const validatePaintProperty = validateStyle.paintProperty;
export const validateLayoutProperty = validateStyle.layoutProperty;
diff --git a/src/symbol/collision_index.ts b/src/symbol/collision_index.ts
index 289f894924e..d188ee4eb89 100644
--- a/src/symbol/collision_index.ts
+++ b/src/symbol/collision_index.ts
@@ -55,6 +55,10 @@ class CollisionIndex {
gridRightBoundary: number;
gridBottomBoundary: number;
+ // With perspectiveRatio the fontsize is calculated for tilted maps (near = bigger, far = smaller).
+ // The cutoff defines a threshold to no longer render labels near the horizon.
+ perspectiveRatioCutoff: number;
+
constructor(
transform: Transform,
grid = new GridIndex(transform.width + 2 * viewportPadding, transform.height + 2 * viewportPadding, 25),
@@ -70,6 +74,8 @@ class CollisionIndex {
this.screenBottomBoundary = transform.height + viewportPadding;
this.gridRightBoundary = transform.width + 2 * viewportPadding;
this.gridBottomBoundary = transform.height + 2 * viewportPadding;
+
+ this.perspectiveRatioCutoff = 0.6;
}
placeCollisionBox(
@@ -77,12 +83,13 @@ class CollisionIndex {
overlapMode: OverlapMode,
textPixelRatio: number,
posMatrix: mat4,
- collisionGroupPredicate?: (key: FeatureKey) => boolean
+ collisionGroupPredicate?: (key: FeatureKey) => boolean,
+ getElevation?: (x: number, y: number) => number
): {
box: Array;
offscreen: boolean;
} {
- const projectedPoint = this.projectAndGetPerspectiveRatio(posMatrix, collisionBox.anchorPointX, collisionBox.anchorPointY);
+ const projectedPoint = this.projectAndGetPerspectiveRatio(posMatrix, collisionBox.anchorPointX, collisionBox.anchorPointY, getElevation);
const tileToViewport = textPixelRatio * projectedPoint.perspectiveRatio;
const tlX = collisionBox.x1 * tileToViewport + projectedPoint.point.x;
const tlY = collisionBox.y1 * tileToViewport + projectedPoint.point.y;
@@ -90,7 +97,8 @@ class CollisionIndex {
const brY = collisionBox.y2 * tileToViewport + projectedPoint.point.y;
if (!this.isInsideGrid(tlX, tlY, brX, brY) ||
- (overlapMode !== 'always' && this.grid.hitTest(tlX, tlY, brX, brY, overlapMode, collisionGroupPredicate))) {
+ (overlapMode !== 'always' && this.grid.hitTest(tlX, tlY, brX, brY, overlapMode, collisionGroupPredicate)) ||
+ projectedPoint.perspectiveRatio < this.perspectiveRatioCutoff) {
return {
box: [],
offscreen: false
@@ -116,7 +124,8 @@ class CollisionIndex {
pitchWithMap: boolean,
collisionGroupPredicate: (key: FeatureKey) => boolean,
circlePixelDiameter: number,
- textPixelPadding: number
+ textPixelPadding: number,
+ getElevation: (x: number, y: number) => number
): {
circles: Array;
offscreen: boolean;
@@ -125,12 +134,12 @@ class CollisionIndex {
const placedCollisionCircles = [];
const tileUnitAnchorPoint = new Point(symbol.anchorX, symbol.anchorY);
- const screenAnchorPoint = projection.project(tileUnitAnchorPoint, posMatrix);
+ const screenAnchorPoint = projection.project(tileUnitAnchorPoint, posMatrix, getElevation);
const perspectiveRatio = projection.getPerspectiveRatio(this.transform.cameraToCenterDistance, screenAnchorPoint.signedDistanceFromCamera);
const labelPlaneFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio;
const labelPlaneFontScale = labelPlaneFontSize / ONE_EM;
- const labelPlaneAnchorPoint = projection.project(tileUnitAnchorPoint, labelPlaneMatrix).point;
+ const labelPlaneAnchorPoint = projection.project(tileUnitAnchorPoint, labelPlaneMatrix, getElevation).point;
const projectionCache = {};
const lineOffsetX = symbol.lineOffsetX * labelPlaneFontScale;
@@ -148,7 +157,8 @@ class CollisionIndex {
lineVertexArray,
labelPlaneMatrix,
projectionCache,
- false);
+ false,
+ getElevation);
let collisionDetected = false;
let inGrid = false;
@@ -178,7 +188,7 @@ class CollisionIndex {
// The path might need to be converted into screen space if a pitched map is used as the label space
if (labelToScreenMatrix) {
- const screenSpacePath = projectedPath.map(p => projection.project(p, labelToScreenMatrix));
+ const screenSpacePath = projectedPath.map(p => projection.project(p, labelToScreenMatrix, getElevation));
// Do not try to place collision circles if even of the points is behind the camera.
// This is a plausible scenario with big camera pitch angles
@@ -265,7 +275,7 @@ class CollisionIndex {
}
return {
- circles: ((!showCollisionCircles && collisionDetected) || !inGrid) ? [] : placedCollisionCircles,
+ circles: ((!showCollisionCircles && collisionDetected) || !inGrid || perspectiveRatio < this.perspectiveRatioCutoff) ? [] : placedCollisionCircles,
offscreen: entirelyOffscreen,
collisionDetected
};
@@ -354,9 +364,15 @@ class CollisionIndex {
}
}
- projectAndGetPerspectiveRatio(posMatrix: mat4, x: number, y: number) {
- const p = [x, y, 0, 1] as vec4;
- projection.xyTransformMat4(p, p, posMatrix);
+ projectAndGetPerspectiveRatio(posMatrix: mat4, x: number, y: number, getElevation: (x: number, y: number) => number) {
+ let p;
+ if (getElevation) { // slow because of handle z-index
+ p = [x, y, getElevation(x, y), 1] as vec4;
+ vec4.transformMat4(p, p, posMatrix);
+ } else { // fast because of ignore z-index
+ p = [x, y, 0, 1] as vec4;
+ projection.xyTransformMat4(p, p, posMatrix);
+ }
const a = new Point(
(((p[0] / p[3] + 1) / 2) * this.transform.width) + viewportPadding,
(((-p[1] / p[3] + 1) / 2) * this.transform.height) + viewportPadding
diff --git a/src/symbol/placement.ts b/src/symbol/placement.ts
index 64801c9bd4a..bc826c3a3d4 100644
--- a/src/symbol/placement.ts
+++ b/src/symbol/placement.ts
@@ -22,6 +22,7 @@ import type {CollisionBoxArray, CollisionVertexArray, SymbolInstance} from '../d
import type FeatureIndex from '../data/feature_index';
import type {OverscaledTileID} from '../source/tile_id';
import type {TextAnchor} from './symbol_layout';
+import Terrain from '../render/terrain';
class OpacityState {
opacity: number;
@@ -210,6 +211,7 @@ export type CrossTileID = string | number;
export class Placement {
transform: Transform;
+ terrain: Terrain;
collisionIndex: CollisionIndex;
placements: {
[_ in CrossTileID]: JointPlacement;
@@ -238,8 +240,9 @@ export class Placement {
[k in any]: CollisionCircleArray;
};
- constructor(transform: Transform, fadeDuration: number, crossSourceCollisions: boolean, prevPlacement?: Placement) {
+ constructor(transform: Transform, terrain: Terrain, fadeDuration: number, crossSourceCollisions: boolean, prevPlacement?: Placement) {
this.transform = transform.clone();
+ this.terrain = terrain;
this.collisionIndex = new CollisionIndex(this.transform);
this.placements = {};
this.opacities = {};
@@ -350,7 +353,8 @@ export class Placement {
symbolInstance: SymbolInstance,
bucket: SymbolBucket,
orientation: number,
- iconBox?: SingleCollisionBox | null
+ iconBox?: SingleCollisionBox | null,
+ getElevation?: (x: number, y: number) => number
): {
shift: Point;
placedGlyphBoxes: {
@@ -366,14 +370,14 @@ export class Placement {
shiftVariableCollisionBox(
textBox, shift.x, shift.y,
rotateWithMap, pitchWithMap, this.transform.angle),
- textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
+ textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate, getElevation);
if (iconBox) {
const placedIconBoxes = this.collisionIndex.placeCollisionBox(
shiftVariableCollisionBox(
iconBox, shift.x, shift.y,
rotateWithMap, pitchWithMap, this.transform.angle),
- textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
+ textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate, getElevation);
if (placedIconBoxes.box.length === 0) return;
}
@@ -489,6 +493,14 @@ export class Placement {
verticalTextFeatureIndex = collisionArrays.verticalTextFeatureIndex;
}
+ // update elevation of collisionArrays
+ const tileID = this.retainedQueryData[bucket.bucketInstanceId].tileID;
+ const getElevation = this.terrain ? (x: number, y: number) => this.terrain.getElevation(tileID, x, y) : null;
+ for (const boxType of ['textBox', 'verticalTextBox', 'iconBox', 'verticalIconBox']) {
+ const box = collisionArrays[boxType];
+ if (box) box.elevation = getElevation ? getElevation(box.anchorPointX, box.anchorPointY) : 0;
+ }
+
const textBox = collisionArrays.textBox;
if (textBox) {
@@ -528,7 +540,9 @@ export class Placement {
textOverlapMode,
textPixelRatio,
posMatrix,
- collisionGroup.predicate);
+ collisionGroup.predicate,
+ getElevation
+ );
if (placedFeature && placedFeature.box && placedFeature.box.length) {
this.markUsedOrientation(bucket, orientation, symbolInstance);
this.placedOrientations[symbolInstance.crossTileID] = orientation;
@@ -583,7 +597,7 @@ export class Placement {
const result = this.attemptAnchorPlacement(
anchor, collisionTextBox, width, height,
textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, posMatrix,
- collisionGroup, overlapMode, symbolInstance, bucket, orientation, variableIconBox);
+ collisionGroup, overlapMode, symbolInstance, bucket, orientation, variableIconBox, getElevation);
if (result) {
placedBox = result.placedGlyphBoxes;
@@ -658,7 +672,9 @@ export class Placement {
pitchWithMap,
collisionGroup.predicate,
circlePixelDiameter,
- textPixelPadding);
+ textPixelPadding,
+ getElevation
+ );
assert(!placedGlyphCircles.circles.length || (!placedGlyphCircles.collisionDetected || showCollisionBoxes));
// If text-overlap is set to 'always', force "placedCircles" to true
@@ -674,7 +690,6 @@ export class Placement {
}
if (collisionArrays.iconBox) {
-
const placeIconFeature = iconBox => {
const shiftedIconBox = hasIconTextFit && shift ?
shiftVariableCollisionBox(
@@ -682,7 +697,7 @@ export class Placement {
rotateWithMap, pitchWithMap, this.transform.angle) :
iconBox;
return this.collisionIndex.placeCollisionBox(shiftedIconBox,
- iconOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
+ iconOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate, getElevation);
};
if (placedVerticalText && placedVerticalText.box && placedVerticalText.box.length && collisionArrays.verticalIconBox) {
diff --git a/src/symbol/projection.ts b/src/symbol/projection.ts
index 9608b1a54df..8cbd90d175b 100644
--- a/src/symbol/projection.ts
+++ b/src/symbol/projection.ts
@@ -101,9 +101,15 @@ function getGlCoordMatrix(posMatrix: mat4,
}
}
-function project(point: Point, matrix: mat4) {
- const pos = [point.x, point.y, 0, 1] as vec4;
- xyTransformMat4(pos, pos, matrix);
+function project(point: Point, matrix: mat4, getElevation: (x: number, y: number) => number) {
+ let pos;
+ if (getElevation) { // slow because of handle z-index
+ pos = [point.x, point.y, getElevation(point.x, point.y), 1] as vec4;
+ vec4.transformMat4(pos, pos, matrix);
+ } else { // fast because of ignore z-index
+ pos = [point.x, point.y, 0, 1] as vec4;
+ xyTransformMat4(pos, pos, matrix);
+ }
const w = pos[3];
return {
point: new Point(pos[0] / w, pos[1] / w),
@@ -139,7 +145,8 @@ function updateLineLabels(bucket: SymbolBucket,
glCoordMatrix: mat4,
pitchWithMap: boolean,
keepUpright: boolean,
- rotateToLine: boolean) {
+ rotateToLine: boolean,
+ getElevation: (x: number, y: number) => number) {
const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData;
const partiallyEvaluatedSize = symbolSize.evaluateSizeForZoom(sizeData, painter.transform.zoom);
@@ -171,8 +178,14 @@ function updateLineLabels(bucket: SymbolBucket,
// Awkward... but we're counting on the paired "vertical" symbol coming immediately after its horizontal counterpart
useVertical = false;
- const anchorPos = [symbol.anchorX, symbol.anchorY, 0, 1] as vec4;
- vec4.transformMat4(anchorPos, anchorPos, posMatrix);
+ let anchorPos;
+ if (getElevation) { // slow because of handle z-index
+ anchorPos = [symbol.anchorX, symbol.anchorY, getElevation(symbol.anchorX, symbol.anchorY), 1] as vec4;
+ vec4.transformMat4(anchorPos, anchorPos, posMatrix);
+ } else { // fast because of ignore z-index
+ anchorPos = [symbol.anchorX, symbol.anchorY, 0, 1] as vec4;
+ xyTransformMat4(anchorPos, anchorPos, posMatrix);
+ }
// Don't bother calculating the correct point for invisible labels.
if (!isVisible(anchorPos, clippingBuffer)) {
@@ -187,18 +200,18 @@ function updateLineLabels(bucket: SymbolBucket,
const pitchScaledFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio;
const tileAnchorPoint = new Point(symbol.anchorX, symbol.anchorY);
- const anchorPoint = project(tileAnchorPoint, labelPlaneMatrix).point;
+ const anchorPoint = project(tileAnchorPoint, labelPlaneMatrix, getElevation).point;
const projectionCache = {};
const placeUnflipped: any = placeGlyphsAlongLine(symbol, pitchScaledFontSize, false /*unflipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix,
- bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine);
+ bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine, getElevation);
useVertical = placeUnflipped.useVertical;
if (placeUnflipped.notEnoughRoom || useVertical ||
(placeUnflipped.needsFlipping &&
(placeGlyphsAlongLine(symbol, pitchScaledFontSize, true /*flipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix,
- bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine) as any).notEnoughRoom)) {
+ bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine, getElevation) as any).notEnoughRoom)) {
hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray);
}
}
@@ -210,7 +223,7 @@ function updateLineLabels(bucket: SymbolBucket,
}
}
-function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffsetArray, lineOffsetX: number, lineOffsetY: number, flip: boolean, anchorPoint: Point, tileAnchorPoint: Point, symbol: any, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, projectionCache: any, rotateToLine: boolean) {
+function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffsetArray, lineOffsetX: number, lineOffsetY: number, flip: boolean, anchorPoint: Point, tileAnchorPoint: Point, symbol: any, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, projectionCache: any, rotateToLine: boolean, getElevation: (x: number, y: number) => number) {
const glyphEndIndex = symbol.glyphStartIndex + symbol.numGlyphs;
const lineStartIndex = symbol.lineStartIndex;
const lineEndIndex = symbol.lineStartIndex + symbol.lineLength;
@@ -219,12 +232,12 @@ function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffset
const lastGlyphOffset = glyphOffsetArray.getoffsetX(glyphEndIndex - 1);
const firstPlacedGlyph = placeGlyphAlongLine(fontScale * firstGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment,
- lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine);
+ lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine, getElevation);
if (!firstPlacedGlyph)
return null;
const lastPlacedGlyph = placeGlyphAlongLine(fontScale * lastGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment,
- lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine);
+ lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine, getElevation);
if (!lastPlacedGlyph)
return null;
@@ -252,7 +265,7 @@ function requiresOrientationChange(writingMode, firstPoint, lastPoint, aspectRat
return null;
}
-function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine) {
+function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine, getElevation) {
const fontScale = fontSize / 24;
const lineOffsetX = symbol.lineOffsetX * fontScale;
const lineOffsetY = symbol.lineOffsetY * fontScale;
@@ -265,12 +278,12 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
// Place the first and the last glyph in the label first, so we can figure out
// the overall orientation of the label and determine whether it needs to be flipped in keepUpright mode
- const firstAndLastGlyph = placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine);
+ const firstAndLastGlyph = placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine, getElevation);
if (!firstAndLastGlyph) {
return {notEnoughRoom: true};
}
- const firstPoint = project(firstAndLastGlyph.first.point, glCoordMatrix).point;
- const lastPoint = project(firstAndLastGlyph.last.point, glCoordMatrix).point;
+ const firstPoint = project(firstAndLastGlyph.first.point, glCoordMatrix, getElevation).point;
+ const lastPoint = project(firstAndLastGlyph.last.point, glCoordMatrix, getElevation).point;
if (keepUpright && !flip) {
const orientationChange = requiresOrientationChange(symbol.writingMode, firstPoint, lastPoint, aspectRatio);
@@ -284,24 +297,24 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
// Since first and last glyph fit on the line, we're sure that the rest of the glyphs can be placed
// $FlowFixMe
placedGlyphs.push(placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(glyphIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment,
- lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine));
+ lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine, getElevation));
}
placedGlyphs.push(firstAndLastGlyph.last);
} else {
// Only a single glyph to place
// So, determine whether to flip based on projected angle of the line segment it's on
if (keepUpright && !flip) {
- const a = project(tileAnchorPoint, posMatrix).point;
+ const a = project(tileAnchorPoint, posMatrix, getElevation).point;
const tileVertexIndex = (symbol.lineStartIndex + symbol.segment + 1);
// $FlowFixMe
const tileSegmentEnd = new Point(lineVertexArray.getx(tileVertexIndex), lineVertexArray.gety(tileVertexIndex));
- const projectedVertex = project(tileSegmentEnd, posMatrix);
+ const projectedVertex = project(tileSegmentEnd, posMatrix, getElevation);
// We know the anchor will be in the viewport, but the end of the line segment may be
// behind the plane of the camera, in which case we can use a point at any arbitrary (closer)
// point on the segment.
const b = (projectedVertex.signedDistanceFromCamera > 0) ?
projectedVertex.point :
- projectTruncatedLineSegment(tileAnchorPoint, tileSegmentEnd, a, 1, posMatrix);
+ projectTruncatedLineSegment(tileAnchorPoint, tileSegmentEnd, a, 1, posMatrix, getElevation);
const orientationChange = requiresOrientationChange(symbol.writingMode, a, b, aspectRatio);
if (orientationChange) {
@@ -310,7 +323,7 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
}
// $FlowFixMe
const singleGlyph = placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(symbol.glyphStartIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment,
- symbol.lineStartIndex, symbol.lineStartIndex + symbol.lineLength, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine);
+ symbol.lineStartIndex, symbol.lineStartIndex + symbol.lineLength, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine, getElevation);
if (!singleGlyph)
return {notEnoughRoom: true};
@@ -323,18 +336,19 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
return {};
}
-function projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint: Point, previousProjectedPoint: Point, minimumLength: number, projectionMatrix: mat4) {
+function projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint: Point, previousProjectedPoint: Point, minimumLength: number, projectionMatrix: mat4, getElevation: (x: number, y: number) => number) {
// We are assuming "previousTilePoint" won't project to a point within one unit of the camera plane
// If it did, that would mean our label extended all the way out from within the viewport to a (very distant)
// point near the plane of the camera. We wouldn't be able to render the label anyway once it crossed the
// plane of the camera.
- const projectedUnitVertex = project(previousTilePoint.add(previousTilePoint.sub(currentTilePoint)._unit()), projectionMatrix).point;
+ const projectedUnitVertex = project(previousTilePoint.add(previousTilePoint.sub(currentTilePoint)._unit()), projectionMatrix, getElevation).point;
const projectedUnitSegment = previousProjectedPoint.sub(projectedUnitVertex);
return previousProjectedPoint.add(projectedUnitSegment._mult(minimumLength / projectedUnitSegment.mag()));
}
-function placeGlyphAlongLine(offsetX: number,
+function placeGlyphAlongLine(
+ offsetX: number,
lineOffsetX: number,
lineOffsetY: number,
flip: boolean,
@@ -348,7 +362,8 @@ function placeGlyphAlongLine(offsetX: number,
projectionCache: {
[_: number]: Point;
},
- rotateToLine: boolean) {
+ rotateToLine: boolean,
+ getElevation: (x: number, y: number) => number) {
const combinedOffsetX = flip ?
offsetX - lineOffsetX :
@@ -390,7 +405,7 @@ function placeGlyphAlongLine(offsetX: number,
current = projectionCache[currentIndex];
if (current === undefined) {
const currentVertex = new Point(lineVertexArray.getx(currentIndex), lineVertexArray.gety(currentIndex));
- const projection = project(currentVertex, labelPlaneMatrix);
+ const projection = project(currentVertex, labelPlaneMatrix, getElevation);
if (projection.signedDistanceFromCamera > 0) {
current = projectionCache[currentIndex] = projection.point;
} else {
@@ -401,7 +416,7 @@ function placeGlyphAlongLine(offsetX: number,
tileAnchorPoint :
new Point(lineVertexArray.getx(previousLineVertexIndex), lineVertexArray.gety(previousLineVertexIndex));
// Don't cache because the new vertex might not be far enough out for future glyphs on the same segment
- current = projectTruncatedLineSegment(previousTilePoint, currentVertex, prev, absOffsetX - distanceToPrev + 1, labelPlaneMatrix);
+ current = projectTruncatedLineSegment(previousTilePoint, currentVertex, prev, absOffsetX - distanceToPrev + 1, labelPlaneMatrix, getElevation);
}
}
diff --git a/src/ui/camera.ts b/src/ui/camera.ts
index fa40c0eed21..97a4214bfbf 100644
--- a/src/ui/camera.ts
+++ b/src/ui/camera.ts
@@ -898,6 +898,7 @@ abstract class Camera extends Evented {
_prepareEase(eventData: any, noMoveStart: boolean, currently: any = {}) {
this._moving = true;
+ this.fire(new Event('freezeElevation', {freeze: true}));
if (!noMoveStart && !currently.moving) {
this.fire(new Event('movestart', eventData));
@@ -933,6 +934,7 @@ abstract class Camera extends Evented {
return;
}
delete this._easeId;
+ this.fire(new Event('freezeElevation', {freeze: false}));
const wasZooming = this._zooming;
const wasRotating = this._rotating;
diff --git a/src/ui/control/terrain_control.ts b/src/ui/control/terrain_control.ts
new file mode 100644
index 00000000000..e819357f725
--- /dev/null
+++ b/src/ui/control/terrain_control.ts
@@ -0,0 +1,77 @@
+import DOM from '../../util/dom';
+import {bindAll} from '../../util/util';
+
+import type Map from '../map';
+import type {IControl} from './control';
+import type {TerrainSpecification} from '../../style-spec/types.g';
+
+/**
+ * An `TerrainControl` control adds a button to turn terrain on and off.
+ *
+ * @implements {IControl}
+ * @param {Object} [options]
+ * @param {string} [options.id] The ID of the raster-dem source to use.
+ * @param {exaggeration: number; elevationOffset: number} [options.options] Allowed options are exaggeration: number; elevationOffset: number
+ * @example
+ * var map = new maplibregl.Map({TerrainControl: false})
+ * .addControl(new maplibregl.TerrainControl({
+ * source: "terrain"
+ * }));
+ */
+class TerrainControl implements IControl {
+ options: TerrainSpecification;
+ _map: Map;
+ _container: HTMLElement;
+ _terrainButton: HTMLButtonElement;
+
+ constructor(options: TerrainSpecification) {
+ this.options = options;
+
+ bindAll([
+ '_toggleTerrain',
+ '_updateTerrainIcon',
+ ], this);
+ }
+
+ onAdd(map: Map) {
+ this._map = map;
+ this._container = DOM.create('div', 'maplibregl-ctrl maplibregl-ctrl-group mapboxgl-ctrl mapboxgl-ctrl-group');
+ this._terrainButton = DOM.create('button', 'maplibregl-ctrl-terrain mapboxgl-ctrl-terrain', this._container);
+ DOM.create('span', 'maplibregl-ctrl-icon mapboxgl-ctrl-icon', this._terrainButton).setAttribute('aria-hidden', 'true');
+ this._terrainButton.type = 'button';
+ this._terrainButton.addEventListener('click', this._toggleTerrain);
+
+ this._updateTerrainIcon();
+ this._map.on('terrain', this._updateTerrainIcon);
+ return this._container;
+ }
+
+ onRemove() {
+ DOM.remove(this._container);
+ this._map.off('terrain', this._updateTerrainIcon);
+ this._map = undefined;
+ }
+
+ _toggleTerrain() {
+ if (this._map.getTerrain()) {
+ this._map.setTerrain(null);
+ } else {
+ this._map.setTerrain(this.options);
+ }
+ this._updateTerrainIcon();
+ }
+
+ _updateTerrainIcon() {
+ this._terrainButton.classList.remove('maplibregl-ctrl-terrain', 'mapboxgl-ctrl-terrain');
+ this._terrainButton.classList.remove('maplibregl-ctrl-terrain-enabled', 'mapboxgl-ctrl-terrain-enabled');
+ if (this._map.style.terrain) {
+ this._terrainButton.classList.add('maplibregl-ctrl-terrain-enabled', 'mapboxgl-ctrl-terrain-enabled');
+ this._terrainButton.title = this._map._getUIString('TerrainControl.disableTerrain');
+ } else {
+ this._terrainButton.classList.add('maplibregl-ctrl-terrain', 'mapboxgl-ctrl-terrain');
+ this._terrainButton.title = this._map._getUIString('TerrainControl.enableTerrain');
+ }
+ }
+}
+
+export default TerrainControl;
diff --git a/src/ui/default_locale.ts b/src/ui/default_locale.ts
index 3c29809a922..a3ea2bc0176 100644
--- a/src/ui/default_locale.ts
+++ b/src/ui/default_locale.ts
@@ -13,8 +13,9 @@ const defaultLocale = {
'ScaleControl.Meters': 'm',
'ScaleControl.Kilometers': 'km',
'ScaleControl.Miles': 'mi',
- 'ScaleControl.NauticalMiles': 'nm'
-
+ 'ScaleControl.NauticalMiles': 'nm',
+ 'TerrainControl.enableTerrain': 'Enable terrain',
+ 'TerrainControl.disableTerrain': 'Disable terrain'
};
export default defaultLocale;
diff --git a/src/ui/events.ts b/src/ui/events.ts
index 2f785279edc..41b33c7dee5 100644
--- a/src/ui/events.ts
+++ b/src/ui/events.ts
@@ -311,6 +311,10 @@ export type MapDataEvent = {
sourceDataType: MapSourceDataType;
};
+export type MapTerrainEvent = {
+ type: 'terrain';
+};
+
export type MapContextEvent = {
type: 'webglcontextlost' | 'webglcontextrestored';
originalEvent: WebGLContextEvent;
@@ -386,6 +390,8 @@ export type MapEventType = {
pitchend: MapLibreEvent;
wheel: MapWheelEvent;
+
+ terrain: MapTerrainEvent;
};
export type MapEvent =
@@ -1395,6 +1401,15 @@ export type MapEvent =
| 'style.load'
/**
+ * @event terrain
+ * @memberof Map
+ * @instance
+ * @private
+ */
+ | 'terrain'
+
+ /**
+ * Fired when a request for one of the map's sources' tiles is aborted.
* Fired when a request for one of the map's sources' data is aborted.
* See {@link MapDataEvent} for more information.
*
diff --git a/src/ui/handler_manager.ts b/src/ui/handler_manager.ts
index 027f85f977a..9ffa6b7260e 100644
--- a/src/ui/handler_manager.ts
+++ b/src/ui/handler_manager.ts
@@ -18,6 +18,7 @@ import DragRotateHandler from './handler/shim/drag_rotate';
import TouchZoomRotateHandler from './handler/shim/touch_zoom_rotate';
import {bindAll, extend} from '../util/util';
import Point from '@mapbox/point-geometry';
+import LngLat from '../geo/lng_lat';
import assert from 'assert';
export type InputEvent = MouseEvent | TouchEvent | KeyboardEvent | WheelEvent;
@@ -100,6 +101,7 @@ class HandlerManager {
_handlersById: {[x: string]: Handler};
_updatingCamera: boolean;
_changes: Array<[HandlerResult, any, any]>;
+ _drag: {center: Point; lngLat: LngLat; point: Point; handlerName: string};
_previousActiveHandlers: {[x: string]: Handler};
_listeners: Array<[Window | Document | HTMLElement, string, {
passive?: boolean;
@@ -411,11 +413,11 @@ class HandlerManager {
}
_updateMapTransform(combinedResult: any, combinedEventsInProgress: any, deactivatedHandlers: any) {
-
const map = this._map;
const tr = map.transform;
+ const terrain = map.style && map.style.terrain;
- if (!hasChange(combinedResult)) {
+ if (!hasChange(combinedResult) && !(terrain && this._drag)) {
return this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true);
}
@@ -433,7 +435,35 @@ class HandlerManager {
if (bearingDelta) tr.bearing += bearingDelta;
if (pitchDelta) tr.pitch += pitchDelta;
if (zoomDelta) tr.zoom += zoomDelta;
- tr.setLocationAtPoint(loc, around);
+
+ if (!terrain) {
+ tr.setLocationAtPoint(loc, around);
+ } else {
+ // when 3d-terrain is enabled act a litte different:
+ // - draging do not drag the picked point itself, instead it drags the map by pixel-delta.
+ // With this approach it is no longer possible to pick a point from somewhere near
+ // the horizon to the center in one move.
+ // So this logic avoids the problem, that in such cases you easily loose orientation.
+ // - scrollzoom does not zoom into the mouse-point, instead it zooms into map-center
+ // this should be fixed in future-version
+ // when dragging starts, remember mousedown-location and panDelta from this point
+ if (combinedEventsInProgress.drag && !this._drag) {
+ this._drag = {
+ center: tr.centerPoint,
+ lngLat: tr.pointLocation(around),
+ point: around,
+ handlerName: combinedEventsInProgress.drag.handlerName
+ };
+ map.fire(new Event('freezeElevation', {freeze: true}));
+ // when dragging ends, recalcuate the zoomlevel for the new center coordinate
+ } else if (this._drag && deactivatedHandlers[this._drag.handlerName]) {
+ map.fire(new Event('freezeElevation', {freeze: false}));
+ this._drag = null;
+ // drag map
+ } else if (combinedEventsInProgress.drag && this._drag) {
+ tr.center = tr.pointLocation(tr.centerPoint.sub(panDelta));
+ }
+ }
this._map._update();
if (!combinedResult.noInertia) this._inertia.record(combinedResult);
diff --git a/src/ui/map.ts b/src/ui/map.ts
index ada8f3b4b84..ff941768468 100644
--- a/src/ui/map.ts
+++ b/src/ui/map.ts
@@ -51,7 +51,8 @@ import type {
FilterSpecification,
StyleSpecification,
LightSpecification,
- SourceSpecification
+ SourceSpecification,
+ TerrainSpecification
} from '../style-spec/types.g';
import {Callback} from '../types/callback';
import type {ControlPosition, IControl} from './control/control';
@@ -432,6 +433,10 @@ class Map extends Camera {
this.on('move', () => this._update(false));
this.on('moveend', () => this._update(false));
this.on('zoom', () => this._update(true));
+ this.on('terrain', () => {
+ this.painter.terrainFacilitator.dirty = true;
+ this._update(true);
+ });
if (typeof window !== 'undefined') {
addEventListener('online', this._onWindowOnline, false);
@@ -873,7 +878,7 @@ class Map extends Camera {
* var point = map.project(coordinate);
*/
project(lnglat: LngLatLike) {
- return this.transform.locationPoint(LngLat.convert(lnglat));
+ return this.transform.locationPoint(LngLat.convert(lnglat), this.style && this.style.terrain);
}
/**
@@ -889,7 +894,7 @@ class Map extends Camera {
* });
*/
unproject(point: PointLike) {
- return this.transform.pointLocation(Point.convert(point));
+ return this.transform.pointLocation(Point.convert(point), this.style && this.style.terrain);
}
/**
@@ -1571,6 +1576,28 @@ class Map extends Camera {
return source.loaded();
}
+ /**
+ * Loads a 3D terrain mesh, based on a "raster-dem" source.
+ * @param {TerrainSpecification} [options] Options object.
+ * @returns {Map} `this`
+ * @example
+ * map.setTerrain({ source: 'terrain' });
+ */
+ setTerrain(options: TerrainSpecification): Map {
+ this.style.setTerrain(options);
+ return this;
+ }
+
+ /**
+ * Get the terrain-options if terrain is loaded
+ * @returns {TerrainSpecification} the TerrainSpecification passed to setTerrain
+ * @example
+ * map.getTerrain(); // { source: 'terrain' };
+ */
+ getTerrain(): TerrainSpecification {
+ return this.style.terrain && this.style.terrain.options;
+ }
+
/**
* Returns a Boolean indicating whether all tiles in the viewport from all sources on
* the style are loaded.
@@ -1579,8 +1606,7 @@ class Map extends Camera {
* @example
* var tilesLoaded = map.areTilesLoaded();
*/
-
- areTilesLoaded() {
+ areTilesLoaded(): boolean {
const sources = this.style && this.style.sourceCaches;
for (const id in sources) {
const source = sources[id];
@@ -1614,7 +1640,7 @@ class Map extends Camera {
* @example
* map.removeSource('bathymetry-data');
*/
- removeSource(id: string) {
+ removeSource(id: string): Map {
this.style.removeSource(id);
return this._update(true);
}
@@ -2550,6 +2576,10 @@ class Map extends Camera {
this.style._updateSources(this.transform);
}
+ // update terrain stuff
+ if (this.style.terrain) this.style.terrain.sourceCache.update(this.transform, this.style.terrain);
+ this.transform.updateElevation(this.style.terrain);
+
this._placementDirty = this.style && this.style._updatePlacement(this.painter.transform, this.showCollisionBoxes, this._fadeDuration, this._crossSourceCollisions);
// Actually draw
diff --git a/src/ui/marker.test.ts b/src/ui/marker.test.ts
index 46c2a0f60b6..62aebe1a39f 100644
--- a/src/ui/marker.test.ts
+++ b/src/ui/marker.test.ts
@@ -4,6 +4,7 @@ import Popup from './popup';
import LngLat from '../geo/lng_lat';
import Point from '@mapbox/point-geometry';
import simulate from '../../test/unit/lib/simulate_interaction';
+import type Terrain from '../render/terrain';
function createMap(options = {}) {
const container = window.document.createElement('div');
@@ -771,4 +772,24 @@ describe('marker', () => {
map.remove();
});
+
+ test('Marker removed after update when terrain is on should clear timeout', () => {
+ jest.spyOn(global, 'setTimeout');
+ jest.spyOn(global, 'clearTimeout');
+ const map = createMap();
+ const marker = new Marker()
+ .setLngLat([0, 0])
+ .addTo(map);
+ map.style.terrain = {
+ getElevation: () => 0
+ } as any as Terrain;
+
+ marker.setOffset([10, 10]);
+
+ expect(setTimeout).toHaveBeenCalled();
+ marker.remove();
+ expect(clearTimeout).toHaveBeenCalled();
+
+ map.remove();
+ });
});
diff --git a/src/ui/marker.ts b/src/ui/marker.ts
index f615512e3af..333cd7a4c69 100644
--- a/src/ui/marker.ts
+++ b/src/ui/marker.ts
@@ -74,6 +74,7 @@ export default class Marker extends Evented {
_pitchAlignment: string;
_rotationAlignment: string;
_originalTabIndex: string; // original tabindex of _element
+ _opacityTimeout: ReturnType;
constructor(options?: MarkerOptions, legacyOptions?: MarkerOptions) {
super();
@@ -264,6 +265,10 @@ export default class Marker extends Evented {
* @returns {Marker} `this`
*/
remove() {
+ if (this._opacityTimeout) {
+ clearTimeout(this._opacityTimeout);
+ delete this._opacityTimeout;
+ }
if (this._map) {
this._map.off('click', this._onMapClick);
this._map.off('move', this._update);
@@ -469,6 +474,15 @@ export default class Marker extends Evented {
}
DOM.setTransform(this._element, `${anchorTranslate[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px) ${pitch} ${rotation}`);
+
+ // in case of 3D, ask the terrain coords-framebuffer for this pos and check if the marker is visible
+ // call this logic in setTimeout with a timeout of 100ms to save performance in map-movement
+ if (this._map.style && this._map.style.terrain && !this._opacityTimeout) this._opacityTimeout = setTimeout(() => {
+ const lnglat = this._map.unproject(this._pos);
+ const metresPerPixel = 40075016.686 * Math.abs(Math.cos(this._lngLat.lat * Math.PI / 180)) / Math.pow(2, this._map.transform.tileZoom + 8);
+ this._element.style.opacity = lnglat.distanceTo(this._lngLat) > metresPerPixel * 20 ? '0.2' : '1.0';
+ this._opacityTimeout = null;
+ }, 100);
}
/**
diff --git a/src/util/primitives.ts b/src/util/primitives.ts
index 6af9a8f3200..44a4bab31c2 100644
--- a/src/util/primitives.ts
+++ b/src/util/primitives.ts
@@ -1,5 +1,4 @@
import {mat4, vec3, vec4} from 'gl-matrix';
-import assert from 'assert';
class Frustum {
@@ -19,10 +18,12 @@ class Frustum {
const scale = Math.pow(2, zoom);
- // Transform frustum corner points from clip space to tile space
- const frustumCoords = clipSpaceCorners
- .map(v => vec4.transformMat4([] as any, v as any, invProj))
- .map(v => vec4.scale([] as any, v, 1.0 / v[3] / worldSize * scale));
+ // Transform frustum corner points from clip space to tile space, Z to meters
+ const frustumCoords = clipSpaceCorners.map(v => {
+ v = vec4.transformMat4([] as any, v as any, invProj) as any;
+ const s = 1.0 / v[3] / worldSize * scale;
+ return vec4.mul(v as any, v as any, [s, s, 1.0 / v[3], s] as vec4);
+ });
const frustumPlanePointIndices = [
[0, 1, 2], // near
@@ -84,14 +85,16 @@ class Aabb {
intersects(frustum: Frustum): number {
// Execute separating axis test between two convex objects to find intersections
// Each frustum plane together with 3 major axes define the separating axes
- // Note: test only 4 points as both min and max points have equal elevation
- assert(this.min[2] === 0 && this.max[2] === 0);
const aabbPoints = [
- [this.min[0], this.min[1], 0.0, 1],
- [this.max[0], this.min[1], 0.0, 1],
- [this.max[0], this.max[1], 0.0, 1],
- [this.min[0], this.max[1], 0.0, 1]
+ [this.min[0], this.min[1], this.min[2], 1],
+ [this.max[0], this.min[1], this.min[2], 1],
+ [this.max[0], this.max[1], this.min[2], 1],
+ [this.min[0], this.max[1], this.min[2], 1],
+ [this.min[0], this.min[1], this.max[2], 1],
+ [this.max[0], this.min[1], this.max[2], 1],
+ [this.max[0], this.max[1], this.max[2], 1],
+ [this.min[0], this.max[1], this.max[2], 1]
];
let fullyInside = true;
diff --git a/test/integration/render/render.test.ts b/test/integration/render/render.test.ts
index 700cca07fa5..f82acb44865 100644
--- a/test/integration/render/render.test.ts
+++ b/test/integration/render/render.test.ts
@@ -63,6 +63,7 @@ type TestData = {
queryGeometry: PointLike;
queryOptions: any;
error: Error;
+ maxPitch: number;
// base64-encoded content of the PNG results
actual: string;
@@ -419,6 +420,7 @@ function getImageFromStyle(style: StyleWithTestData): Promise {
classes: options.classes,
interactive: false,
attributionControl: false,
+ maxPitch: options.maxPitch,
pixelRatio: options.pixelRatio,
preserveDrawingBuffer: true,
axonometric: options.axonometric || false,
diff --git a/test/integration/render/tests/debug/collision-horizon-cutoff/expected.png b/test/integration/render/tests/debug/collision-horizon-cutoff/expected.png
new file mode 100644
index 00000000000..73ffb2d58b4
Binary files /dev/null and b/test/integration/render/tests/debug/collision-horizon-cutoff/expected.png differ
diff --git a/test/integration/render/tests/debug/collision-horizon-cutoff/style.json b/test/integration/render/tests/debug/collision-horizon-cutoff/style.json
new file mode 100644
index 00000000000..9b9b6f256f7
--- /dev/null
+++ b/test/integration/render/tests/debug/collision-horizon-cutoff/style.json
@@ -0,0 +1,49 @@
+{
+ "version": 8,
+ "metadata": {
+ "test": {
+ "collisionDebug": true,
+ "width": 500,
+ "height": 500,
+ "allowed": 0.005,
+ "maxPitch": 85
+ }
+ },
+ "center": [
+ -60,
+ 0
+ ],
+ "zoom": 5,
+ "pitch": 75,
+ "bearing": 0,
+ "sources": {
+ "geojson": {
+ "type": "geojson",
+ "data": "local://data/places.geojson"
+ }
+ },
+ "glyphs": "local://glyphs/{fontstack}/{range}.pbf",
+ "sprite": "local://sprites/sprite",
+ "layers": [
+ {
+ "id": "background",
+ "type": "background",
+ "paint": {
+ "background-color": "white"
+ }
+ },
+ {
+ "id": "symbol",
+ "type": "symbol",
+ "source": "geojson",
+ "layout": {
+ "symbol-placement": "point",
+ "text-field": "test test test",
+ "text-font": [
+ "Open Sans Semibold",
+ "Arial Unicode MS Bold"
+ ]
+ }
+ }
+ ]
+ }
diff --git a/test/integration/render/tests/debug/collision-icon-text-point-translate/expected.png b/test/integration/render/tests/debug/collision-icon-text-point-translate/expected.png
index bb2a67c4964..4786207dedd 100644
Binary files a/test/integration/render/tests/debug/collision-icon-text-point-translate/expected.png and b/test/integration/render/tests/debug/collision-icon-text-point-translate/expected.png differ
diff --git a/test/integration/render/tests/debug/collision-pitched-wrapped/expected.png b/test/integration/render/tests/debug/collision-pitched-wrapped/expected.png
index d23243aa8ae..6d3c1d4678b 100644
Binary files a/test/integration/render/tests/debug/collision-pitched-wrapped/expected.png and b/test/integration/render/tests/debug/collision-pitched-wrapped/expected.png differ
diff --git a/test/integration/render/tests/debug/collision-pitched/expected.png b/test/integration/render/tests/debug/collision-pitched/expected.png
index d1eb3bf6a38..45e8ce2982b 100644
Binary files a/test/integration/render/tests/debug/collision-pitched/expected.png and b/test/integration/render/tests/debug/collision-pitched/expected.png differ
diff --git a/test/integration/render/tests/debug/collision/expected.png b/test/integration/render/tests/debug/collision/expected.png
index 6dc313d9531..8e5b6b994d8 100644
Binary files a/test/integration/render/tests/debug/collision/expected.png and b/test/integration/render/tests/debug/collision/expected.png differ