UNPKG

mapbox-gl

Version:
1,166 lines (996 loc) 75 kB
// @flow import {FillExtrusionGroundLayoutArray, FillExtrusionLayoutArray, FillExtrusionExtArray, FillExtrusionCentroidArray, FillExtrusionHiddenByLandmarkArray, PosArray} from '../array_types.js'; import {members as layoutAttributes, fillExtrusionGroundAttributes, centroidAttributes, fillExtrusionAttributesExt, hiddenByLandmarkAttributes} from './fill_extrusion_attributes.js'; import SegmentVector from '../segment.js'; import {ProgramConfigurationSet} from '../program_configuration.js'; import {TriangleIndexArray} from '../index_array_type.js'; import EXTENT from '../../style-spec/data/extent.js'; import earcut from 'earcut'; import {VectorTileFeature} from '@mapbox/vector-tile'; const vectorTileFeatureTypes = VectorTileFeature.types; import classifyRings from '../../util/classify_rings.js'; import assert from 'assert'; const EARCUT_MAX_RINGS = 500; import {register} from '../../util/web_worker_transfer.js'; import {hasPattern, addPatternDependencies} from './pattern_bucket_features.js'; import loadGeometry from '../load_geometry.js'; import toEvaluationFeature from '../evaluation_feature.js'; import EvaluationParameters from '../../style/evaluation_parameters.js'; import Point from '@mapbox/point-geometry'; import {number as interpolate} from '../../style-spec/util/interpolate.js'; import {lngFromMercatorX, latFromMercatorY, mercatorYfromLat, tileToMeter} from '../../geo/mercator_coordinate.js'; import {subdividePolygons} from '../../util/polygon_clipping.js'; import {ReplacementSource, regionsEquals, footprintTrianglesIntersect} from '../../../3d-style/source/replacement_source.js'; import {clamp} from '../../util/util.js'; import {earthRadius} from '../../geo/lng_lat.js'; import {Aabb, Frustum} from '../../util/primitives.js'; import {Elevation} from '../../terrain/elevation.js'; import type {Feature} from "../../style-spec/expression"; import type {ClippedPolygon} from '../../util/polygon_clipping.js'; import type {Vec3} from 'gl-matrix'; import type {CanonicalTileID, OverscaledTileID} from '../../source/tile_id.js'; import type {Segment} from '../segment.js'; import type { Bucket, BucketParameters, BucketFeature, IndexedFeature, PopulateParameters } from '../bucket.js'; import type FillExtrusionStyleLayer from '../../style/style_layer/fill_extrusion_style_layer.js'; import type Context from '../../gl/context.js'; import type IndexBuffer from '../../gl/index_buffer.js'; import type VertexBuffer from '../../gl/vertex_buffer.js'; import type {FeatureStates} from '../../source/source_state.js'; import type {SpritePositions} from '../../util/image.js'; import type {ProjectionSpecification} from '../../style-spec/types.js'; import type {TileTransform} from '../../geo/projection/tile_transform.js'; import type {IVectorTileLayer} from '@mapbox/vector-tile'; export const fillExtrusionDefaultDataDrivenProperties: Array<string> = [ 'fill-extrusion-base', 'fill-extrusion-height', 'fill-extrusion-color', 'fill-extrusion-pattern', 'fill-extrusion-flood-light-wall-radius' ]; export const fillExtrusionGroundDataDrivenProperties: Array<string> = [ 'fill-extrusion-flood-light-ground-radius' ]; const FACTOR = Math.pow(2, 13); const TANGENT_CUTOFF = 4; const NORM = Math.pow(2, 15) - 1; const QUAD_VERTS = 4; const QUAD_TRIS = 2; // In flood lighting a line segment is extruded based on the flood light radius to form a quad. // The tile is divided into four regions (left, right, top and bottom). // As an example when a quad crosses the left border it belongs to the left region. const TILE_REGIONS = 4; const HIDDEN_CENTROID: Point = new Point(0, 1); export const HIDDEN_BY_REPLACEMENT: number = 0x80000000; // Also declared in _prelude_terrain.vertex.glsl // Used to scale most likely elevation values to fit well in an uint16 // (Elevation of Dead Sea + ELEVATION_OFFSET) * ELEVATION_SCALE is roughly 0 // (Height of mt everest + ELEVATION_OFFSET) * ELEVATION_SCALE is roughly 64k export const ELEVATION_SCALE = 7.0; export const ELEVATION_OFFSET = 450; function addVertex(vertexArray: FillExtrusionLayoutArray, x: number, y: number, nxRatio: number, nySign: number, normalUp: number, top: number, e: number) { vertexArray.emplaceBack( // a_pos_normal_ed: // Encode top and side/up normal using the least significant bits (x << 1) + top, (y << 1) + normalUp, // dxdy is signed, encode quadrant info using the least significant bit (Math.floor(nxRatio * FACTOR) << 1) + nySign, // edgedistance (used for wrapping patterns around extrusion sides) Math.round(e) ); } function addGroundVertex(vertexArray: FillExtrusionGroundLayoutArray, p: Point, q: Point, start: number, bottom: number, angle: number) { vertexArray.emplaceBack( p.x, p.y, (q.x << 1) + start, (q.y << 1) + bottom, angle ); } function addGlobeExtVertex(vertexArray: FillExtrusionExtArray, pos: {x: number, y: number, z: number}, normal: Vec3) { const encode = 1 << 14; vertexArray.emplaceBack( pos.x, pos.y, pos.z, normal[0] * encode, normal[1] * encode, normal[2] * encode); } class FootprintSegment { vertexOffset: number; vertexCount: number; indexOffset: number; indexCount: number; ringIndices: Array<number>; constructor() { this.vertexOffset = 0; this.vertexCount = 0; this.indexOffset = 0; this.indexCount = 0; } } // Stores centroid buffer content (one entry per feature as opposite to one entry per // vertex in the buffer). This information is used to do conflation vs 3d model layers. // PartData and BorderCentroidData are split because PartData is stored for every // bucket feature and BorderCentroidData only for features that intersect border. export class PartData { centroidXY: Point; vertexArrayOffset: number; vertexCount: number; groundVertexArrayOffset: number; groundVertexCount: number; flags: number; footprintSegIdx: number; footprintSegLen: number; polygonSegIdx: number; polygonSegLen: number; min: Point; max: Point; height: number; constructor() { this.centroidXY = new Point(0, 0); this.vertexArrayOffset = 0; this.vertexCount = 0; this.groundVertexArrayOffset = 0; this.groundVertexCount = 0; this.flags = 0; this.footprintSegIdx = -1; this.footprintSegLen = 0; this.polygonSegIdx = -1; this.polygonSegLen = 0; this.min = new Point(Number.MAX_VALUE, Number.MAX_VALUE); this.max = new Point(-Number.MAX_VALUE, -Number.MAX_VALUE); this.height = 0; } span(): Point { return new Point(this.max.x - this.min.x, this.max.y - this.min.y); } } // Used for calculating centroid of a feature and intersections of a feature with tile borders. // Uses and extends data in PartData. References to PartData via centroidDataIndex. class BorderCentroidData { acc: Point; accCount: number; borders: ?Array<[number, number]>; // Array<[min, max]> centroidDataIndex: number; constructor() { this.acc = new Point(0, 0); this.accCount = 0; this.centroidDataIndex = 0; } startRing(data: PartData, p: Point) { if (data.min.x === Number.MAX_VALUE) { // If not initialized. data.min.x = data.max.x = p.x; data.min.y = data.max.y = p.y; } } appendEdge(data: PartData, p: Point, prev: Point) { assert(data.min.x !== Number.MAX_VALUE); this.accCount++; this.acc._add(p); let checkBorders = !!this.borders; if (p.x < data.min.x) { data.min.x = p.x; checkBorders = true; } else if (p.x > data.max.x) { data.max.x = p.x; checkBorders = true; } if (p.y < data.min.y) { data.min.y = p.y; checkBorders = true; } else if (p.y > data.max.y) { data.max.y = p.y; checkBorders = true; } if (((p.x === 0 || p.x === EXTENT) && p.x === prev.x) !== ((p.y === 0 || p.y === EXTENT) && p.y === prev.y)) { // Custom defined geojson buildings are cut on borders. Points are // repeated when edge cuts tile corner (reason for using xor). this.processBorderOverlap(p, prev); } if (checkBorders) { this.checkBorderIntersection(p, prev); } } checkBorderIntersection(p: Point, prev: Point) { if ((prev.x < 0) !== (p.x < 0)) { this.addBorderIntersection(0, interpolate(prev.y, p.y, (0 - prev.x) / (p.x - prev.x))); } if ((prev.x > EXTENT) !== (p.x > EXTENT)) { this.addBorderIntersection(1, interpolate(prev.y, p.y, (EXTENT - prev.x) / (p.x - prev.x))); } if ((prev.y < 0) !== (p.y < 0)) { this.addBorderIntersection(2, interpolate(prev.x, p.x, (0 - prev.y) / (p.y - prev.y))); } if ((prev.y > EXTENT) !== (p.y > EXTENT)) { this.addBorderIntersection(3, interpolate(prev.x, p.x, (EXTENT - prev.y) / (p.y - prev.y))); } } addBorderIntersection(index: 0 | 1 | 2 | 3, i: number) { if (!this.borders) { this.borders = [ [Number.MAX_VALUE, -Number.MAX_VALUE], [Number.MAX_VALUE, -Number.MAX_VALUE], [Number.MAX_VALUE, -Number.MAX_VALUE], [Number.MAX_VALUE, -Number.MAX_VALUE] ]; } const b = this.borders[index]; if (i < b[0]) b[0] = i; if (i > b[1]) b[1] = i; } processBorderOverlap(p: Point, prev: Point) { if (p.x === prev.x) { if (p.y === prev.y) return; // custom defined geojson could have points repeated. const index = p.x === 0 ? 0 : 1; this.addBorderIntersection(index, prev.y); this.addBorderIntersection(index, p.y); } else { assert(p.y === prev.y); const index = p.y === 0 ? 2 : 3; this.addBorderIntersection(index, prev.x); this.addBorderIntersection(index, p.x); } } centroid(): Point { if (this.accCount === 0) { return new Point(0, 0); } return new Point( Math.floor(Math.max(0, this.acc.x) / this.accCount), Math.floor(Math.max(0, this.acc.y) / this.accCount)); } intersectsCount(): number { if (!this.borders) { return 0; } return this.borders.reduce((acc, p) => acc + +(p[0] !== Number.MAX_VALUE), 0); } } function concavity(a: Point, b: Point) { return a.x * b.y - a.y * b.x < 0 ? -1 : 1; } function tanAngleClamped(angle: number) { return Math.min(TANGENT_CUTOFF, Math.max(-TANGENT_CUTOFF, Math.tan(angle))) / TANGENT_CUTOFF * NORM; } function getAngularOffsetFactor(na: Point, nb: Point): number { const nm = na.add(nb)._unit(); const cosHalfAngle = clamp(na.x * nm.x + na.y * nm.y, -1, 1); const factor = tanAngleClamped(Math.acos(cosHalfAngle)) * concavity(na, nb); return factor; } const borderCheck = [ (a: Point): boolean => { return a.x < 0; }, // left (a: Point): boolean => { return a.x > EXTENT; }, // right (a: Point): boolean => { return a.y < 0; }, // top (a: Point): boolean => { return a.y > EXTENT; } // bottom ]; // Checks which region a quad belongs to. A quad belongs to left, right, top, bottom if // it intersects with the left, right, top, bottom borders of the tile respectively. // If a quad doesn't intersect any of the borders, it is assumed to be in the "default" region. // Ids 0, 1, 2, 3 and 4 are denoting, left, right, top, bottom and default regions respectively. // Note that a quad can also belong to more than one region (e.g. when it intersects with left and right borders). function getTileRegions(pa: Point, pb: Point, na: Point, maxRadius: number) { const regions = [4]; if (maxRadius === 0) return regions; // Approximate the position of the extruded points by using a quad. na._mult(maxRadius); const c = pa.sub(na); const d = pb.sub(na); const points = [pa, pb, c, d]; for (let i = 0; i < TILE_REGIONS; i++) { for (const point of points) { if (borderCheck[i](point)) { regions.push(i); break; } } } return regions; } type GroundQuad = { id: number; region: number; // 0 - left, 1 - right, 2 - top, 3 - bottom, 4 - rest } export class GroundEffect { vertexArray: FillExtrusionGroundLayoutArray; vertexBuffer: VertexBuffer; hiddenByLandmarkVertexArray: FillExtrusionHiddenByLandmarkArray; hiddenByLandmarkVertexBuffer: VertexBuffer; _needsHiddenByLandmarkUpdate: boolean; indexArray: TriangleIndexArray; indexBuffer: IndexBuffer; _segments: SegmentVector; _segmentToGroundQuads: {[number]: Array<GroundQuad>}; _segmentToRegionTriCounts: {[number]: Array<number>}; regionSegments: {[number]: ?SegmentVector}; programConfigurations: ProgramConfigurationSet<FillExtrusionStyleLayer>; constructor(options: BucketParameters<FillExtrusionStyleLayer>) { this.vertexArray = new FillExtrusionGroundLayoutArray(); this.indexArray = new TriangleIndexArray(); const filtered = (property: string) => { return fillExtrusionGroundDataDrivenProperties.includes(property); }; this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom, filtered); this._segments = new SegmentVector(); this.hiddenByLandmarkVertexArray = new FillExtrusionHiddenByLandmarkArray(); this._segmentToGroundQuads = {}; this._segmentToGroundQuads[0] = []; this._segmentToRegionTriCounts = {}; this._segmentToRegionTriCounts[0] = [0, 0, 0, 0, 0]; this.regionSegments = {}; this.regionSegments[4] = new SegmentVector(); } getDefaultSegment(): any { return this.regionSegments[4]; } hasData(): boolean { return this.vertexArray.length !== 0; } addData(polyline: Array<Point>, bounds: [Point, Point], maxRadius: number, roundedEdges: boolean = false) { const n = polyline.length; if (n > 2) { let sid = Math.max(0, this._segments.get().length - 1); const numNewVerts = n * 4; const numExistingVerts = this.vertexArray.length; const numExistingTris = this._segmentToGroundQuads[sid].length * QUAD_TRIS; const segment = this._segments._prepareSegment(numNewVerts, numExistingVerts, numExistingTris); const newSegmentAdded = sid !== this._segments.get().length - 1; if (newSegmentAdded) { sid++; this._segmentToGroundQuads[sid] = []; this._segmentToRegionTriCounts[sid] = [0, 0, 0, 0, 0]; } let prevFactor; { const pa = polyline[n - 1]; const pb = polyline[0]; const pc = polyline[1]; const na = pb.sub(pa)._perp()._unit(); const nb = pc.sub(pb)._perp()._unit(); prevFactor = getAngularOffsetFactor(na, nb); } for (let i = 0; i < n; i++) { const j = i === n - 1 ? 0 : i + 1; const k = j === n - 1 ? 0 : j + 1; const pa = polyline[i]; const pb = polyline[j]; const pc = polyline[k]; const na = pb.sub(pa)._perp()._unit(); const nb = pc.sub(pb)._perp()._unit(); const factor = getAngularOffsetFactor(na, nb); const a0 = prevFactor; const a1 = factor; if (isEdgeOutsideBounds(pa, pb, bounds) || (roundedEdges && pointOutsideBounds(pa, bounds) && pointOutsideBounds(pb, bounds))) { prevFactor = factor; continue; } const idx = segment.vertexLength; addGroundVertex(this.vertexArray, pa, pb, 1, 1, a0); addGroundVertex(this.vertexArray, pa, pb, 1, 0, a0); addGroundVertex(this.vertexArray, pa, pb, 0, 1, a1); addGroundVertex(this.vertexArray, pa, pb, 0, 0, a1); segment.vertexLength += QUAD_VERTS; // When a tile belongs to more than one region it needs to be duplicated for that region. const regions = getTileRegions(pa, pb, na, maxRadius); // Note: mutates na for (const rid of regions) { this._segmentToGroundQuads[sid].push({ id: idx, region: rid }); this._segmentToRegionTriCounts[sid][rid] += QUAD_TRIS; segment.primitiveLength += QUAD_TRIS; } prevFactor = factor; } } } prepareBorderSegments() { if (!this.hasData()) return; assert(this._segments && this._segmentToGroundQuads && this._segmentToRegionTriCounts); assert(!this.indexArray.length); const segments = this._segments.get(); // Sort the geometry in this order: left, right, top, bottom and default regions. const numSegments = segments.length; for (let i = 0; i < numSegments; i++) { const groundQuads = this._segmentToGroundQuads[i]; groundQuads.sort((a: GroundQuad, b: GroundQuad) => { return a.region - b.region; }); } // Populate the index array. for (let i = 0; i < numSegments; i++) { const groundQuads = this._segmentToGroundQuads[i]; const segment = segments[i]; const regionTriCounts = this._segmentToRegionTriCounts[i]; const totalTriCount = regionTriCounts.reduce((acc: number, a: number) => { return acc + a; }, 0); assert(segment.primitiveLength === totalTriCount); // For each segment create 5 additional segments each representing a region. let regionTriCountOffset = 0; for (let k = 0; k <= TILE_REGIONS; k++) { const triCount = regionTriCounts[k]; assert(triCount % QUAD_TRIS === 0); if (triCount !== 0) { let segmentVector = this.regionSegments[k]; // Lazy initialise the segment vector. During rendering if no such vector exists // it means that no geometry from this tile intersects the corresponding border. // We therefore can skip the said border to speed up rendering. if (!segmentVector) { segmentVector = this.regionSegments[k] = new SegmentVector(); } const nSegment: any = { vertexOffset: segment.vertexOffset, primitiveOffset: segment.primitiveOffset + regionTriCountOffset, vertexLength: segment.vertexLength, primitiveLength: triCount }; segmentVector.get().push(nSegment); } regionTriCountOffset += triCount; } for (let j = 0; j < groundQuads.length; j++) { const groundQuad = groundQuads[j]; const idx = groundQuad.id; this.indexArray.emplaceBack(idx, idx + 1, idx + 3); this.indexArray.emplaceBack(idx, idx + 3, idx + 2); } } // Free up memory as we no longer need these. this._segmentToGroundQuads = (null: any); this._segmentToRegionTriCounts = (null: any); this._segments.destroy(); this._segments = (null: any); } addPaintPropertiesData(feature: Feature, index: number, imagePositions: SpritePositions, availableImages: Array<string>, canonical: CanonicalTileID, brightness: ?number) { if (!this.hasData()) return; this.programConfigurations.populatePaintArrays(this.vertexArray.length, feature, index, imagePositions, availableImages, canonical, brightness); } upload(context: Context) { if (!this.hasData()) return; this.vertexBuffer = context.createVertexBuffer(this.vertexArray, fillExtrusionGroundAttributes.members); this.indexBuffer = context.createIndexBuffer(this.indexArray); } uploadPaintProperties(context: Context) { if (!this.hasData()) return; this.programConfigurations.upload(context); } update(states: FeatureStates, vtLayer: IVectorTileLayer, layers: any, availableImages: Array<string>, imagePositions: SpritePositions, brightness: ?number) { if (!this.hasData()) return; this.programConfigurations.updatePaintArrays(states, vtLayer, layers, availableImages, imagePositions, brightness); } updateHiddenByLandmark(data: PartData) { if (!this.hasData()) return; const offset = data.groundVertexArrayOffset; const vertexArrayBounds = data.groundVertexCount + data.groundVertexArrayOffset; assert(vertexArrayBounds <= this.hiddenByLandmarkVertexArray.length); assert(this.hiddenByLandmarkVertexArray.length === this.vertexArray.length); if (data.groundVertexCount === 0) return; const hide = data.flags & HIDDEN_BY_REPLACEMENT ? 1 : 0; for (let i = offset; i < vertexArrayBounds; ++i) { this.hiddenByLandmarkVertexArray.emplace(i, hide); } this._needsHiddenByLandmarkUpdate = true; } uploadHiddenByLandmark(context: Context) { if (!this.hasData() || !this._needsHiddenByLandmarkUpdate) { return; } if (!this.hiddenByLandmarkVertexBuffer && this.hiddenByLandmarkVertexArray.length > 0) { // Create centroids vertex buffer this.hiddenByLandmarkVertexBuffer = context.createVertexBuffer(this.hiddenByLandmarkVertexArray, hiddenByLandmarkAttributes.members, true); } else if (this.hiddenByLandmarkVertexBuffer) { this.hiddenByLandmarkVertexBuffer.updateData(this.hiddenByLandmarkVertexArray); } this._needsHiddenByLandmarkUpdate = false; } destroy() { if (!this.vertexBuffer) return; this.vertexBuffer.destroy(); this.indexBuffer.destroy(); if (this.hiddenByLandmarkVertexBuffer) { this.hiddenByLandmarkVertexBuffer.destroy(); } if (this._segments) this._segments.destroy(); this.programConfigurations.destroy(); for (let i = 0; i <= TILE_REGIONS; i++) { const segments = this.regionSegments[i]; if (segments) { segments.destroy(); } } } } type PolygonSegment = { triangleArrayOffset: number; triangleCount: number; triangleSegIdx: number; }; type SegmentedFeature = { centroidIdx: number; subtile: number; polygonSegmentIdx: number; triangleSegmentIdx: number; }; type TriangleSubSegment = { segment: Segment; min: Point; max: Point; }; class FillExtrusionBucket implements Bucket { index: number; zoom: number; canonical: CanonicalTileID; overscaling: number; layers: Array<FillExtrusionStyleLayer>; layerIds: Array<string>; stateDependentLayers: Array<FillExtrusionStyleLayer>; stateDependentLayerIds: Array<string>; layoutVertexArray: FillExtrusionLayoutArray; layoutVertexBuffer: VertexBuffer; centroidVertexArray: FillExtrusionCentroidArray; centroidVertexBuffer: VertexBuffer; layoutVertexExtArray: ?FillExtrusionExtArray; layoutVertexExtBuffer: ?VertexBuffer; indexArray: TriangleIndexArray; indexBuffer: IndexBuffer; footprintSegments: Array<FootprintSegment> footprintVertices: PosArray; footprintIndices: TriangleIndexArray; hasPattern: boolean; edgeRadius: number; programConfigurations: ProgramConfigurationSet<FillExtrusionStyleLayer>; segments: SegmentVector; uploaded: boolean; features: Array<BucketFeature>; featuresOnBorder: Array<BorderCentroidData>; borderFeatureIndices: Array<Array<number>>; centroidData: Array<PartData>; // borders / borderDoneWithNeighborZ: 0 - left, 1, right, 2 - top, 3 - bottom borderDoneWithNeighborZ: Array<number>; needsCentroidUpdate: boolean; tileToMeter: number; // cache conversion. projection: ProjectionSpecification; activeReplacements: Array<any>; replacementUpdateTime: number; groundEffect: GroundEffect; partLookup: {[_: number]: ?PartData}; maxHeight: number; triangleSubSegments: Array<TriangleSubSegment>; polygonSegments: Array<PolygonSegment>; constructor(options: BucketParameters<FillExtrusionStyleLayer>) { this.zoom = options.zoom; this.canonical = options.canonical; this.overscaling = options.overscaling; this.layers = options.layers; this.layerIds = this.layers.map(layer => layer.fqid); this.index = options.index; this.hasPattern = false; this.edgeRadius = 0; this.projection = options.projection; this.activeReplacements = []; this.replacementUpdateTime = 0; this.centroidData = []; this.footprintIndices = new TriangleIndexArray(); this.footprintVertices = new PosArray(); this.footprintSegments = []; this.layoutVertexArray = new FillExtrusionLayoutArray(); this.centroidVertexArray = new FillExtrusionCentroidArray(); this.indexArray = new TriangleIndexArray(); const filtered = (property: string) => { return fillExtrusionDefaultDataDrivenProperties.includes(property); }; this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom, filtered); this.segments = new SegmentVector(); this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); this.groundEffect = new GroundEffect(options); this.maxHeight = 0; this.partLookup = {}; this.triangleSubSegments = []; this.polygonSegments = []; } populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) { this.features = []; this.hasPattern = hasPattern('fill-extrusion', this.layers, options); this.featuresOnBorder = []; this.borderFeatureIndices = [[], [], [], []]; this.borderDoneWithNeighborZ = [-1, -1, -1, -1]; this.tileToMeter = tileToMeter(canonical); this.edgeRadius = this.layers[0].layout.get('fill-extrusion-edge-radius') / this.tileToMeter; for (const {feature, id, index, sourceLayerIndex} of features) { const needGeometry = this.layers[0]._featureFilter.needGeometry; const evaluationFeature = toEvaluationFeature(feature, needGeometry); // $FlowFixMe[method-unbinding] if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue; const bucketFeature: BucketFeature = { id, sourceLayerIndex, index, geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature, canonical, tileTransform), properties: feature.properties, type: feature.type, patterns: {} }; const vertexArrayOffset = this.layoutVertexArray.length; if (this.hasPattern) { this.features.push(addPatternDependencies('fill-extrusion', this.layers, bucketFeature, this.zoom, options)); } else { this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {}, options.availableImages, tileTransform, options.brightness); } options.featureIndex.insert(feature, bucketFeature.geometry, index, sourceLayerIndex, this.index, vertexArrayOffset); } this.sortBorders(); if (this.projection.name === "mercator") { this.splitToSubtiles(); } this.groundEffect.prepareBorderSegments(); // Clear polygon segment array this.polygonSegments.length = 0; } addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: SpritePositions, availableImages: Array<string>, tileTransform: TileTransform, brightness: ?number) { for (const feature of this.features) { const {geometry} = feature; this.addFeature(feature, geometry, feature.index, canonical, imagePositions, availableImages, tileTransform, brightness); } this.sortBorders(); if (this.projection.name === "mercator") { this.splitToSubtiles(); } } update(states: FeatureStates, vtLayer: IVectorTileLayer, availableImages: Array<string>, imagePositions: SpritePositions, brightness: ?number) { const withStateUpdates = Object.keys(states).length !== 0; if (withStateUpdates && !this.stateDependentLayers.length) return; const layers = withStateUpdates ? this.stateDependentLayers : this.layers; this.programConfigurations.updatePaintArrays(states, vtLayer, layers, availableImages, imagePositions, brightness); this.groundEffect.update(states, vtLayer, layers, availableImages, imagePositions, brightness); } isEmpty(): boolean { return this.layoutVertexArray.length === 0; } uploadPending(): boolean { return !this.uploaded || this.programConfigurations.needsUpload || this.groundEffect.programConfigurations.needsUpload; } upload(context: Context) { if (!this.uploaded) { this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, layoutAttributes); this.indexBuffer = context.createIndexBuffer(this.indexArray); if (this.layoutVertexExtArray) { this.layoutVertexExtBuffer = context.createVertexBuffer(this.layoutVertexExtArray, fillExtrusionAttributesExt.members, true); } this.groundEffect.upload(context); } this.groundEffect.uploadPaintProperties(context); this.programConfigurations.upload(context); this.uploaded = true; } uploadCentroid(context: Context) { this.groundEffect.uploadHiddenByLandmark(context); if (!this.needsCentroidUpdate) { return; } if (!this.centroidVertexBuffer && this.centroidVertexArray.length > 0) { // Create centroids vertex buffer this.centroidVertexBuffer = context.createVertexBuffer(this.centroidVertexArray, centroidAttributes.members, true); } else if (this.centroidVertexBuffer) { this.centroidVertexBuffer.updateData(this.centroidVertexArray); } this.needsCentroidUpdate = false; } destroy() { if (!this.layoutVertexBuffer) return; this.layoutVertexBuffer.destroy(); if (this.centroidVertexBuffer) { this.centroidVertexBuffer.destroy(); } if (this.layoutVertexExtBuffer) { this.layoutVertexExtBuffer.destroy(); } this.groundEffect.destroy(); this.indexBuffer.destroy(); this.programConfigurations.destroy(); this.segments.destroy(); } addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: SpritePositions, availableImages: Array<string>, tileTransform: TileTransform, brightness: ?number) { const floodLightRadius = this.layers[0].paint.get('fill-extrusion-flood-light-ground-radius').evaluate(feature, {}); const maxRadius = floodLightRadius / this.tileToMeter; const tileBounds = [new Point(0, 0), new Point(EXTENT, EXTENT)]; const projection = tileTransform.projection; const isGlobe = projection.name === 'globe'; const isPolygon = vectorTileFeatureTypes[feature.type] === 'Polygon'; const borderCentroidData = new BorderCentroidData(); borderCentroidData.centroidDataIndex = this.centroidData.length; const centroid = new PartData(); const base = this.layers[0].paint.get('fill-extrusion-base').evaluate(feature, {}, canonical); const onGround = base <= 0; const height = this.layers[0].paint.get('fill-extrusion-height').evaluate(feature, {}, canonical); centroid.height = height; centroid.vertexArrayOffset = this.layoutVertexArray.length; centroid.groundVertexArrayOffset = this.groundEffect.vertexArray.length; if (isGlobe && !this.layoutVertexExtArray) { this.layoutVertexExtArray = new FillExtrusionExtArray(); } const polygons = classifyRings(geometry, EARCUT_MAX_RINGS); for (let i = polygons.length - 1; i >= 0; i--) { const polygon = polygons[i]; if (polygon.length === 0 || isEntirelyOutside(polygon[0])) { polygons.splice(i, 1); } } let clippedPolygons: ClippedPolygon[]; if (isGlobe) { // Perform tesselation for polygons of tiles in order to support long planar // triangles on the curved surface of the globe. This is done for all polygons // regardless of their size in order guarantee identical results on all sides of // tile boundaries. // // The globe is subdivided into a 32x16 grid. The number of subdivisions done // for a tile depends on the zoom level. For example tile with z=0 requires 2⁴ // subdivisions, tile with z=1 2³ etc. The subdivision is done in polar coordinates // instead of tile coordinates. clippedPolygons = resampleFillExtrusionPolygonsForGlobe(polygons, tileBounds, canonical); } else { clippedPolygons = []; for (const polygon of polygons) { clippedPolygons.push({polygon, bounds: tileBounds}); } } const edgeRadius = isPolygon ? this.edgeRadius : 0; const optimiseGround = edgeRadius > 0 && this.zoom < 17; const isDuplicate = (coords: Array<Point>, a: Point) => { if (coords.length === 0) return false; const b = coords[coords.length - 1]; return a.x === b.x && a.y === b.y; }; for (const {polygon, bounds} of clippedPolygons) { // Only triangulate and draw the area of the feature if it is a polygon // Other feature types (e.g. LineString) do not have area, so triangulation is pointless / undefined let topIndex = 0; let numVertices = 0; for (const ring of polygon) { // make sure the ring closes if (isPolygon && !ring[0].equals(ring[ring.length - 1])) ring.push(ring[0]); numVertices += (isPolygon ? (ring.length - 1) : ring.length); } // We use "(isPolygon ? 5 : 4) * numVertices" as an estimate to ensure whether additional segments are needed or not (see SegmentVector.MAX_VERTEX_ARRAY_LENGTH). const segment = this.segments.prepareSegment((isPolygon ? 5 : 4) * numVertices, this.layoutVertexArray, this.indexArray); if (centroid.footprintSegIdx < 0) { centroid.footprintSegIdx = this.footprintSegments.length; } if (centroid.polygonSegIdx < 0) { centroid.polygonSegIdx = this.polygonSegments.length; } // Store location of generated triangles for the future use const polygonSeg = {triangleArrayOffset: this.indexArray.length, triangleCount: 0, triangleSegIdx: this.segments.segments.length - 1}; const fpSegment = new FootprintSegment(); fpSegment.vertexOffset = this.footprintVertices.length; fpSegment.indexOffset = this.footprintIndices.length * 3; fpSegment.ringIndices = []; if (isPolygon) { const flattened = []; const holeIndices = []; topIndex = segment.vertexLength; // First we offset (inset) the top vertices (i.e the vertices that make up the roof). for (let r = 0; r < polygon.length; r++) { const ring = polygon[r]; if (ring.length && r !== 0) { holeIndices.push(flattened.length / 2); } // Geometry used by ground flood light and AO. const groundPolyline: Array<Point> = []; // The following vectors are used to avoid duplicate normal calculations when going over the vertices. let na, nb; { const p0 = ring[0]; const p1 = ring[1]; na = p1.sub(p0)._perp()._unit(); } // Store index to the end of this ring, we substract one because we don't add the last point to the // footprint as it's the same as the first one fpSegment.ringIndices.push(ring.length - 1); for (let i = 1; i < ring.length; i++) { const p1 = ring[i]; const p2 = ring[i === ring.length - 1 ? 1 : i + 1]; const q = p1.clone(); if (edgeRadius) { nb = p2.sub(p1)._perp()._unit(); const nm = na.add(nb)._unit(); const cosHalfAngle = na.x * nm.x + na.y * nm.y; const offset = edgeRadius * Math.min(4, 1 / cosHalfAngle); q.x += offset * nm.x; q.y += offset * nm.y; q.x = Math.round(q.x); q.y = Math.round(q.y); na = nb; } if (onGround && (edgeRadius === 0 || optimiseGround) && !isDuplicate(groundPolyline, q)) { groundPolyline.push(q); } addVertex(this.layoutVertexArray, q.x, q.y, 0, 0, 1, 1, 0); segment.vertexLength++; this.footprintVertices.emplaceBack(p1.x, p1.y); // triangulate as if vertices were not offset to ensure correct triangulation flattened.push(p1.x, p1.y); if (isGlobe) { const array: any = this.layoutVertexExtArray; const projectedP = projection.projectTilePoint(q.x, q.y, canonical); const n = projection.upVector(canonical, q.x, q.y); addGlobeExtVertex(array, projectedP, n); } } if (onGround && (edgeRadius === 0 || optimiseGround)) { if (groundPolyline.length !== 0 && isDuplicate(groundPolyline, groundPolyline[0])) { groundPolyline.pop(); } this.groundEffect.addData(groundPolyline, bounds, maxRadius); } } const indices = earcut(flattened, holeIndices); assert(indices.length % 3 === 0); for (let j = 0; j < indices.length; j += 3) { this.footprintIndices.emplaceBack( fpSegment.vertexOffset + indices[j + 0], fpSegment.vertexOffset + indices[j + 1], fpSegment.vertexOffset + indices[j + 2]); // clockwise winding order. this.indexArray.emplaceBack( topIndex + indices[j], topIndex + indices[j + 2], topIndex + indices[j + 1]); segment.primitiveLength++; } fpSegment.indexCount += indices.length; fpSegment.vertexCount += this.footprintVertices.length - fpSegment.vertexOffset; } for (let r = 0; r < polygon.length; r++) { const ring = polygon[r]; borderCentroidData.startRing(centroid, ring[0]); let isPrevCornerConcave = ring.length > 4 && isAOConcaveAngle(ring[ring.length - 2], ring[0], ring[1]); let offsetPrev = edgeRadius ? getRoundedEdgeOffset(ring[ring.length - 2], ring[0], ring[1], edgeRadius) : 0; // Geometry used by ground flood light and AO. const groundPolyline: Array<Point> = []; let kFirst; // The following vectors are used to avoid duplicate normal calculations when going over the vertices. let na, nb; { const p0 = ring[0]; const p1 = ring[1]; na = p1.sub(p0)._perp()._unit(); } let cap = true; for (let i = 1, edgeDistance = 0; i < ring.length; i++) { let p0 = ring[i - 1]; let p1 = ring[i]; const p2 = ring[i === ring.length - 1 ? 1 : i + 1]; borderCentroidData.appendEdge(centroid, p1, p0); if (isEdgeOutsideBounds(p1, p0, bounds)) { if (edgeRadius) { na = p2.sub(p1)._perp()._unit(); cap = !cap; } continue; } const d = p1.sub(p0)._perp(); // Given that nz === 0, encode nx / (abs(nx) + abs(ny)) and signs. // This information is sufficient to reconstruct normal vector in vertex shader. const nxRatio = d.x / (Math.abs(d.x) + Math.abs(d.y)); const nySign = d.y > 0 ? 1 : 0; const dist = p0.dist(p1); if (edgeDistance + dist > 32768) edgeDistance = 0; // Next offset the vertices along the edges and create a chamfer space between them: // So if we have the following (where 'x' denotes a vertex) // x──────x // | | // | | // | | // | | // x──────x // we end up with: // x────x // x x // | | // | | // x x // x────x // (drawing isn't exact but hopefully gets the point across). if (edgeRadius) { nb = p2.sub(p1)._perp()._unit(); const cosHalfAngle = getCosHalfAngle(na, nb); let offsetNext = _getRoundedEdgeOffset(p0, p1, p2, cosHalfAngle, edgeRadius); if (isNaN(offsetNext)) offsetNext = 0; const nEdge = p1.sub(p0)._unit(); p0 = p0.add(nEdge.mult(offsetPrev))._round(); p1 = p1.add(nEdge.mult(-offsetNext))._round(); offsetPrev = offsetNext; na = nb; if (onGround && this.zoom >= 17) { if (!isDuplicate(groundPolyline, p0)) groundPolyline.push(p0); if (!isDuplicate(groundPolyline, p1)) groundPolyline.push(p1); } } const k = segment.vertexLength; const isConcaveCorner = ring.length > 4 && isAOConcaveAngle(p0, p1, p2); let encodedEdgeDistance = encodeAOToEdgeDistance(edgeDistance, isPrevCornerConcave, cap); addVertex(this.layoutVertexArray, p0.x, p0.y, nxRatio, nySign, 0, 0, encodedEdgeDistance); addVertex(this.layoutVertexArray, p0.x, p0.y, nxRatio, nySign, 0, 1, encodedEdgeDistance); edgeDistance += dist; encodedEdgeDistance = encodeAOToEdgeDistance(edgeDistance, isConcaveCorner, !cap); isPrevCornerConcave = isConcaveCorner; addVertex(this.layoutVertexArray, p1.x, p1.y, nxRatio, nySign, 0, 0, encodedEdgeDistance); addVertex(this.layoutVertexArray, p1.x, p1.y, nxRatio, nySign, 0, 1, encodedEdgeDistance); segment.vertexLength += 4; // ┌──────┐ // │ 1 3 │ clockwise winding order. // │ │ Triangle 1: 0 => 1 => 2 // │ 0 2 │ Triangle 2: 1 => 3 => 2 // └──────┘ this.indexArray.emplaceBack(k + 0, k + 1, k + 2); this.indexArray.emplaceBack(k + 1, k + 3, k + 2); segment.primitiveLength += 2; if (edgeRadius) { // Note that in the previous for-loop we start from index 1 to add the top vertices which explains the next line. const t0 = topIndex + (i === 1 ? ring.length - 2 : i - 2); const t1 = i === 1 ? topIndex : t0 + 1; // top chamfer along the side (i.e. the space between the wall and the roof). this.indexArray.emplaceBack(k + 1, t0, k + 3); this.indexArray.emplaceBack(t0, t1, k + 3); segment.primitiveLength += 2; if (kFirst === undefined) { kFirst = k; } // Make sure to fill in the gap in the corner only when both corresponding edges are in tile bounds. if (!isEdgeOutsideBounds(p2, ring[i], bounds)) { const l = i === ring.length - 1 ? kFirst : segment.vertexLength; // vertical side chamfer i.e. the space between consecutive walls. this.indexArray.emplaceBack(k + 2, k + 3, l); this.indexArray.emplaceBack(k + 3, l + 1, l); // top corner where the top(roof) and two sides(walls) meet. this.indexArray.emplaceBack(k + 3, t1, l + 1); segment.primitiveLength += 3; } cap = !cap; } if (isGlobe) { const array: any = this.layoutVertexExtArray; const projectedP0 = projection.projectTilePoint(p0.x, p0.y, canonical); const projectedP1 = projection.projectTilePoint(p1.x, p1.y, canonical); const n0 = projection.upVector(canonical, p0.x, p0.y); const n1 = projection.upVector(canonical, p1.x, p1.y); addGlobeExtVertex(array, projectedP0, n0); addGlobeExtVertex(array, projectedP0, n0); addGlobeExtVertex(array, projectedP1, n1); addGlobeExtVertex(array, projectedP1, n1); } } if (isPolygon) topIndex += (ring.length - 1); if (onGround && edgeRadius && this.zoom >= 17) { if (groundPolyline.length !== 0 && isDuplicate(groundPolyline, groundPolyline[0])) { groundPolyline.pop(); } this.groundEffect.addData(groundPolyline, bounds, maxRadius, edgeRadius > 0); } } this.footprintSegments.push(fpSegment); polygonSeg.triangleCount = this.indexArray.length - polygonSeg.triangleArrayOffset; this.polygonSegments.push(polygonSeg); ++centroid.footprintSegLen; ++centroid.polygonSegLen; } assert(!isGlobe || (this.layoutVertexExtArray && this.layoutVertexExtArray.length === this.layoutVertexArray.length)); centroid.vertexCount = this.layoutVertexArray.length - centroid.vertexArrayOffset; centroid.groundVertexCount = this.groundEffect.vertexArray.length - centroid.groundVertexArrayOffset; if (centroid.vertexCount === 0) { return; } // hiddenCentroid {0, 1}: it is initially hidden as borders are processed later. centroid.centroidXY = borderCentroidData.borders ? HIDDEN_CENTROID : this.encodeCentroid(borderCentroidData, centroid); this.centroidData.push(centroid); if (borderCentroidData.borders) { // When building is split between tiles, store information that enables joining. // parts of building that layes in differentt buckets. assert(borderCentroidData.centroidDataIndex === this.centroidData.length - 1); this.featuresOnBorder.push(borderCentroidData); const borderIndex = this.featuresOnBorder.length - 1; for (let i = 0; i < (borderCentroidData.bord