Skip to content

Commit

Permalink
Globe - fill extrusion layer (#3968)
Browse files Browse the repository at this point in the history
* Import changes for fill-extrusion from main vector globe branch

* Fill extrusion: refactor

* Fill extrusion: indent shader ifdefs

* Fill extrusion: add example

* Fill extrusion: update build size

* Move globe specific projection methods to projection interface

* Fix failing unit test

* Use vec3.clone() instead of manually copying vector components
  • Loading branch information
kubapelc authored Apr 11, 2024
1 parent b5d7bba commit bf4a5b5
Show file tree
Hide file tree
Showing 18 changed files with 715 additions and 158 deletions.
233 changes: 134 additions & 99 deletions src/data/bucket/fill_extrusion_bucket.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {FillExtrusionLayoutArray, PosArray} from '../array_types.g';

import {members as layoutAttributes, centroidAttributes} from './fill_extrusion_attributes';
import {SegmentVector} from '../segment';
import {Segment, SegmentVector} from '../segment';
import {ProgramConfigurationSet} from '../program_configuration';
import {TriangleIndexArray} from '../index_array_type';
import {EXTENT} from '../extent';
import earcut from 'earcut';
import mvt from '@mapbox/vector-tile';
const vectorTileFeatureTypes = mvt.VectorTileFeature.types;
import {classifyRings} from '../../util/classify_rings';
Expand Down Expand Up @@ -33,6 +32,9 @@ import type Point from '@mapbox/point-geometry';
import type {FeatureStates} from '../../source/source_state';
import type {ImagePosition} from '../../render/image_atlas';
import type {VectorTileLayer} from '@mapbox/vector-tile';
import {subdividePolygon, subdivideVertexLine} from '../../render/subdivision';
import type {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import {fillLargeMeshArrays} from '../../render/fill_large_mesh_arrays';

const FACTOR = Math.pow(2, 13);

Expand All @@ -50,6 +52,12 @@ function addVertex(vertexArray, x, y, nx, ny, nz, t, e) {
);
}

type CentroidAccumulator = {
x: number;
y: number;
sampleCount: number;
}

export class FillExtrusionBucket implements Bucket {
index: number;
zoom: number;
Expand Down Expand Up @@ -113,7 +121,7 @@ export class FillExtrusionBucket implements Bucket {
if (this.hasPattern) {
this.features.push(addPatternDependencies('fill-extrusion', this.layers, bucketFeature, this.zoom, options));
} else {
this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {});
this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {}, options.subdivisionGranularity);
}

options.featureIndex.insert(feature, bucketFeature.geometry, index, sourceLayerIndex, this.index, true);
Expand All @@ -123,7 +131,7 @@ export class FillExtrusionBucket implements Bucket {
addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) {
for (const feature of this.features) {
const {geometry} = feature;
this.addFeature(feature, geometry, feature.index, canonical, imagePositions);
this.addFeature(feature, geometry, feature.index, canonical, imagePositions, options.subdivisionGranularity);
}
}

Expand Down Expand Up @@ -159,131 +167,158 @@ export class FillExtrusionBucket implements Bucket {
this.centroidVertexBuffer.destroy();
}

addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) {
const centroid = {x: 0, y: 0, vertexCount: 0};
addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, subdivisionGranularity: SubdivisionGranularitySetting) {
// Compute polygon centroid to calculate elevation in GPU
const centroid: CentroidAccumulator = {x: 0, y: 0, sampleCount: 0};

const oldVertexCount = this.layoutVertexArray.length;

for (const polygon of classifyRings(geometry, EARCUT_MAX_RINGS)) {
let numVertices = 0;
for (const ring of polygon) {
numVertices += ring.length;
}
let segment = this.segments.prepareSegment(4, this.layoutVertexArray, this.indexArray);
this.processPolygon(centroid, canonical, feature, polygon, subdivisionGranularity);
}

for (const ring of polygon) {
if (ring.length === 0) {
continue;
}
const addedVertices = this.layoutVertexArray.length - oldVertexCount;

if (isEntirelyOutside(ring)) {
continue;
}
const centroidX = Math.floor(centroid.x / centroid.sampleCount);
const centroidY = Math.floor(centroid.y / centroid.sampleCount);

let edgeDistance = 0;
for (let i = 0; i < addedVertices; i++) {
this.centroidVertexArray.emplaceBack(
centroidX,
centroidY
);
}

for (let p = 0; p < ring.length; p++) {
const p1 = ring[p];
this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, canonical);
}

if (p >= 1) {
const p2 = ring[p - 1];
private processPolygon(
centroid: CentroidAccumulator,
canonical: CanonicalTileID,
feature: BucketFeature,
polygon: Array<Array<Point>>,
subdivisionGranularity: SubdivisionGranularitySetting
): void {
if (polygon.length < 1) {
return;
}

if (!isBoundaryEdge(p1, p2)) {
if (segment.vertexLength + 4 > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) {
segment = this.segments.prepareSegment(4, this.layoutVertexArray, this.indexArray);
}
if (isEntirelyOutside(polygon[0])) {
return;
}

const perp = p1.sub(p2)._perp()._unit();
const dist = p2.dist(p1);
if (edgeDistance + dist > 32768) edgeDistance = 0;
// Only consider the un-subdivided polygon outer ring for centroid calculation
for (const ring of polygon) {
if (ring.length === 0) {
continue;
}

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;
// Here we don't mind if a hole ring is entirely outside, unlike when generating geometry later.
accumulatePointsToCentroid(centroid, ring);
}

edgeDistance += dist;
const segmentReference = {
segment: this.segments.prepareSegment(4, this.layoutVertexArray, this.indexArray)
};
const granularity = subdivisionGranularity.fill.getGranularityForZoomLevel(canonical.z);
const isPolygon = vectorTileFeatureTypes[feature.type] === 'Polygon';

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;
for (const ring of polygon) {
if (ring.length === 0) {
continue;
}

const bottomRight = segment.vertexLength;
if (isEntirelyOutside(ring)) {
continue;
}

// ┌──────┐
// │ 0 1 │ Counter-clockwise winding order.
// │ │ Triangle 1: 0 => 2 => 1
// │ 2 3 │ Triangle 2: 1 => 2 => 3
// └──────┘
this.indexArray.emplaceBack(bottomRight, bottomRight + 2, bottomRight + 1);
this.indexArray.emplaceBack(bottomRight + 1, bottomRight + 2, bottomRight + 3);
const subdividedRing = subdivideVertexLine(ring, granularity, isPolygon);
this._generateSideFaces(subdividedRing, segmentReference);
}

segment.vertexLength += 4;
segment.primitiveLength += 2;
}
}
}
// 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
if (!isPolygon)
return;

// Do not generate outlines, since outlines already got subdivided earlier.
const subdividedPolygon = subdividePolygon(polygon, canonical, granularity, false);
const vertexArray = this.layoutVertexArray;

fillLargeMeshArrays(
(x, y) => {
addVertex(vertexArray, x, y, 0, 0, 1, 1, 0);
},
this.segments,
this.layoutVertexArray,
this.indexArray,
subdividedPolygon.verticesFlattened,
subdividedPolygon.indicesTriangles
);
}

}
/**
* Generates side faces for the supplied geometry. Assumes `geometry` to be a line string, like the output of {@link subdivideVertexLine}.
* For rings, it is assumed that the first and last vertex of `geometry` are equal.
*/
private _generateSideFaces(geometry: Array<Point>, segmentReference: {segment: Segment}) {
let edgeDistance = 0;

if (segment.vertexLength + numVertices > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) {
segment = this.segments.prepareSegment(numVertices, this.layoutVertexArray, this.indexArray);
}
for (let p = 1; p < geometry.length; p++) {
const p1 = geometry[p];
const p2 = geometry[p - 1];

//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
if (vectorTileFeatureTypes[feature.type] !== 'Polygon')
if (isBoundaryEdge(p1, p2)) {
continue;
}

const flattened = [];
const holeIndices = [];
const triangleIndex = segment.vertexLength;
if (segmentReference.segment.vertexLength + 4 > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) {
segmentReference.segment = this.segments.prepareSegment(4, this.layoutVertexArray, this.indexArray);
}

for (const ring of polygon) {
if (ring.length === 0) {
continue;
}
const perp = p1.sub(p2)._perp()._unit();
const dist = p2.dist(p1);
if (edgeDistance + dist > 32768) edgeDistance = 0;

if (ring !== polygon[0]) {
holeIndices.push(flattened.length / 2);
}
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);

for (let i = 0; i < ring.length; i++) {
const p = ring[i];
edgeDistance += dist;

addVertex(this.layoutVertexArray, p.x, p.y, 0, 0, 1, 1, 0);
centroid.x += p.x;
centroid.y += p.y;
centroid.vertexCount += 1;
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);

flattened.push(p.x);
flattened.push(p.y);
}
const bottomRight = segmentReference.segment.vertexLength;

}
// ┌──────┐
// │ 0 1 │ Counter-clockwise winding order.
// │ │ Triangle 1: 0 => 2 => 1
// │ 2 3 │ Triangle 2: 1 => 2 => 3
// └──────┘
this.indexArray.emplaceBack(bottomRight, bottomRight + 2, bottomRight + 1);
this.indexArray.emplaceBack(bottomRight + 1, bottomRight + 2, bottomRight + 3);

const indices = earcut(flattened, holeIndices);
segmentReference.segment.vertexLength += 4;
segmentReference.segment.primitiveLength += 2;
}
}
}

for (let j = 0; j < indices.length; j += 3) {
// Counter-clockwise winding order.
this.indexArray.emplaceBack(
triangleIndex + indices[j],
triangleIndex + indices[j + 2],
triangleIndex + indices[j + 1]);
}
/**
* Accumulates geometry to centroid. Geometry can be either a polygon ring, a line string or a closed line string.
* In case of a polygon ring or line ring, the last vertex is ignored if it is the same as the first vertex.
*/
function accumulatePointsToCentroid(centroid: CentroidAccumulator, geometry: Array<Point>): void {
for (let i = 0; i < geometry.length; i++) {
const p = geometry[i];

segment.primitiveLength += indices.length / 3;
segment.vertexLength += numVertices;
if (i === geometry.length - 1 && geometry[0].x === p.x && geometry[0].y === p.y) {
continue;
}

// 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);
centroid.x += p.x;
centroid.y += p.y;
centroid.sampleCount++;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/geo/projection/globe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('GlobeProjection', () => {
});

test('camera is in positive halfspace', () => {
expect(planeDistance((globe as any)._globeCameraPosition, projectionData.u_projection_clipping_plane)).toBeGreaterThan(0);
expect(planeDistance(globe.cameraPosition as [number, number, number], projectionData.u_projection_clipping_plane)).toBeGreaterThan(0);
});

test('coordinates 0E,0N are in positive halfspace', () => {
Expand Down
8 changes: 4 additions & 4 deletions src/geo/projection/globe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class GlobeProjection implements Projection {
private _globeProjMatrix: mat4 = mat4.create();
private _globeProjMatrixNoCorrection: mat4 = mat4.create();

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

get name(): string {
return 'globe';
Expand All @@ -88,8 +88,8 @@ export class GlobeProjection implements Projection {
return this._globeness > 0.0;
}

get globeCameraPosition(): [number, number, number] {
return [this._globeCameraPosition[0], this._globeCameraPosition[1], this._globeCameraPosition[2]];
get cameraPosition(): vec3 {
return vec3.clone(this._cameraPosition); // Return a copy - don't let outside code mutate our precomputed camera position.
}

/**
Expand Down Expand Up @@ -220,7 +220,7 @@ export class GlobeProjection implements Projection {

const cameraPos: vec4 = [0, 0, -1, 1];
vec4.transformMat4(cameraPos, cameraPos, invProj);
this._globeCameraPosition = [
this._cameraPosition = [
cameraPos[0] / cameraPos[3],
cameraPos[1] / cameraPos[3],
cameraPos[2] / cameraPos[3]
Expand Down
21 changes: 18 additions & 3 deletions src/geo/projection/mercator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {mat4} from 'gl-matrix';
import {mat4, vec3, vec4} from 'gl-matrix';
import {Transform} from '../transform';
import {Projection, ProjectionGPUContext} from './projection';
import {CanonicalTileID, UnwrappedTileID} from '../../source/tile_id';
Expand All @@ -20,6 +20,7 @@ export const MercatorShaderVariantKey = 'mercator';

export class MercatorProjection implements Projection {
private _cachedMesh: Mesh = null;
private _cameraPosition: vec3 = [0, 0, 0];

get name(): string {
return 'mercator';
Expand All @@ -29,6 +30,10 @@ export class MercatorProjection implements Projection {
return false;
}

get cameraPosition(): vec3 {
return vec3.clone(this._cameraPosition); // Return a copy - don't let outside code mutate our precomputed camera position.
}

get drawWrappedTiles(): boolean {
// Mercator always needs to draw wrapped/duplicated tiles.
return true;
Expand Down Expand Up @@ -72,8 +77,14 @@ export class MercatorProjection implements Projection {
// Do nothing.
}

updateProjection(_: Transform): void {
// Do nothing.
updateProjection(t: Transform): void {
const cameraPos: vec4 = [0, 0, -1, 1];
vec4.transformMat4(cameraPos, cameraPos, t.invProjMatrix);
this._cameraPosition = [
cameraPos[0] / cameraPos[3],
cameraPos[1] / cameraPos[3],
cameraPos[2] / cameraPos[3]
];
}

getProjectionData(canonicalTileCoords: {x: number; y: number; z: number}, tilePosMatrix: mat4): ProjectionData {
Expand Down Expand Up @@ -147,6 +158,10 @@ export class MercatorProjection implements Projection {
this._cachedMesh = new Mesh(tileExtentBuffer, quadTriangleIndexBuffer, tileExtentSegments);
return this._cachedMesh;
}

transformLightDirection(_: Transform, dir: vec3): vec3 {
return vec3.clone(dir);
}
}

/**
Expand Down
Loading

0 comments on commit bf4a5b5

Please sign in to comment.