Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Globe - basic infrastructure, raster layer adaptation for globe #3783

Merged
merged 53 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
18765bb
Port changes from main globe branch - basics
kubapelc Feb 29, 2024
864fc05
Fix PI redefinitions
kubapelc Mar 1, 2024
9c443ac
Fix stencil shader
kubapelc Mar 1, 2024
d318ae0
Port adaptation of raster layer for globe from main globe branch
kubapelc Mar 1, 2024
9efa2df
Add globe.html example from pheonor's repo
kubapelc Mar 1, 2024
6062c88
Better map projection parameter doc comment, warn when using unknown …
kubapelc Mar 1, 2024
d03d9f8
Mercator projectionData handles negative zoom correctly
kubapelc Mar 4, 2024
6a4f6df
Comment clarification
kubapelc Mar 4, 2024
8e7b42e
Fix spelling of "granularity"
kubapelc Mar 4, 2024
b18607e
Merge tag 'v4.1.0' into kubapelc/globe-pr
kubapelc Mar 4, 2024
9d2d8f1
Add missing docs
kubapelc Mar 4, 2024
ba1798a
Convert ProjectionBase to an interface
kubapelc Mar 5, 2024
925eff6
Do not leak GL object in globe projection error measurement, add a de…
kubapelc Mar 5, 2024
e20c02f
Fix chrome performance warning, refactor error measurement
kubapelc Mar 5, 2024
cb30ec2
Fix granularity capitalization
kubapelc Mar 5, 2024
c3d6973
Fix capitalization
kubapelc Mar 5, 2024
6428456
Fix typo
kubapelc Mar 5, 2024
abf9dfb
Fix stencil mask triangle index order (this was causing failing rende…
kubapelc Mar 5, 2024
3cc71ca
Cleanup vertex shader projection interface
kubapelc Mar 6, 2024
cf797bc
Move projection creation function into its own file
kubapelc Mar 9, 2024
d4b861e
Remove getProjectionName
kubapelc Mar 9, 2024
532180d
Added comment for deduplicateWrapped
kubapelc Mar 9, 2024
cdb98ec
Remove unused vertex-buffer-related code from image source
kubapelc Mar 11, 2024
e36c1e9
Add globe raster layer render test
kubapelc Mar 12, 2024
3612661
More render tests - test transition to mercator
kubapelc Mar 12, 2024
667d654
Remove pointless test, add test descriptions
kubapelc Mar 12, 2024
01e84e3
Render test for rendering poles on globe
kubapelc Mar 12, 2024
7d6fdb0
SubdivisionGranularitySetting constructor takes an object
kubapelc Mar 13, 2024
89b568c
Remove "defines" parameter from useProgram
kubapelc Mar 13, 2024
1c9ebb1
Refactor useProgram and Program constructor
kubapelc Mar 13, 2024
23533e8
Properly format translatePosMatrix comment
kubapelc Mar 13, 2024
d1d4960
Refactor globe-specific code outside projection classes, remove stenc…
kubapelc Mar 13, 2024
ff58e89
Refactor granularity settings to be more readable
kubapelc Mar 13, 2024
5f0f31f
Minor refactor of ProjectionErrorMeasurement
kubapelc Mar 13, 2024
d6136a7
Refactor draw_raster.ts
kubapelc Mar 13, 2024
31edcdd
Move globe utility functions to utils.ts, use easeCubicInOut instead …
kubapelc Mar 13, 2024
d2048b4
Simplify imports in globe.ts
kubapelc Mar 13, 2024
a0d8e50
globe.ts refactor
kubapelc Mar 13, 2024
c798bc8
Move ProjectionErrorMeasurement to a separate file
kubapelc Mar 13, 2024
3b392fe
Refactor ProjectionErrorMeasurement
kubapelc Mar 14, 2024
f12d996
Refactor draw_raster.ts
kubapelc Mar 14, 2024
80b4dd3
Refactor globe projection error measurement to not use Painter
kubapelc Mar 14, 2024
fbc654e
Painter.clearStencil creates custom ProjectionData instead of calling…
kubapelc Mar 14, 2024
01e55b1
Remove "deduplicateWrapped" functionality from source_cache.ts
kubapelc Mar 14, 2024
20cdfee
Globe projection no longer requires a map instance
kubapelc Mar 14, 2024
7363ed1
Painter doesn't pass `this` to `updateGPUdependent`
kubapelc Mar 14, 2024
560dd34
isRenderingDirty is now a function
kubapelc Mar 14, 2024
7494c02
Rename ProjectionBase to Projection
kubapelc Mar 14, 2024
79bacd0
Replace globeView property with setGlobeViewAllowed
kubapelc Mar 14, 2024
5054de0
Add mercator and globe projection unit tests
kubapelc Mar 14, 2024
8915b67
Remove tests that test for exact clipping planes
kubapelc Mar 14, 2024
d98921d
Update build test with new bundle size
kubapelc Mar 14, 2024
4526c16
isRenderingDirty is now a function
kubapelc Mar 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
339 changes: 45 additions & 294 deletions src/geo/projection/globe.ts

Large diffs are not rendered by default.

239 changes: 239 additions & 0 deletions src/geo/projection/globe_projection_error_measurement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import {Color} from '@maplibre/maplibre-gl-style-spec';
import {ColorMode} from '../../gl/color_mode';
import {Context} from '../../gl/context';
import {CullFaceMode} from '../../gl/cull_face_mode';
import {DepthMode} from '../../gl/depth_mode';
import {StencilMode} from '../../gl/stencil_mode';
import {warnOnce} from '../../util/util';
import {projectionErrorMeasurementUniformValues} from '../../render/program/projection_error_measurement_program';
import {Painter} from '../../render/painter';
import {Mesh} from '../../render/mesh';
import {SegmentVector} from '../../data/segment';
import {PosArray, TriangleIndexArray} from '../../data/array_types.g';
import posAttributes from '../../data/pos_attributes';
import {Framebuffer} from '../../gl/framebuffer';

/**
* For vector globe the vertex shader projects mercator coordinates to angular coordinates on a sphere.
* This projection requires some inverse trigonometry `atan(exp(...))`, which is inaccurate on some GPUs (mainly on AMD and Nvidia).
* The inaccuracy is severe enough to require a workaround. The uncorrected map is shifted north-south by up to several hundred meters in some latitudes.
* Since the inaccuracy is hardware-dependant and may change in the future, we need to measure the error at runtime.
*
* Our approach relies on several assumptions:
*
* - the error is only present in the "latitude" component (longitude doesn't need any inverse trigonometry)
* - the error is continuous and changes slowly with latitude
* - at zoom levels where the error is noticeable, the error is more-or-less the same across the entire visible map area (and thus can be described with a single number)
*
* Solution:
*
* Every few frames, launch a GPU shader that measures the error for the current map center latitude, and writes it to a 1x1 texture.
* Read back that texture, and offset the globe projection matrix according to the error (interpolating smoothly from old error to new error if needed).
* The texture readback is done asynchronously using Pixel Pack Buffers (WebGL2) when possible, and has a few frames of latency, but that should not be a problem.
*
* General operation of this class each frame is:
*
* - render the error shader into a fbo, read that pixel into a PBO, place a fence
* - wait a few frames to allow the GPU (and driver) to actually execute the shader
* - wait for the fence to be signalled (guaranteeing the shader to actually be executed)
* - read back the PBO's contents
* - wait a few more frames
* - repeat
*/
export class ProjectionErrorMeasurement {
// We wait at least this many frames after measuring until we read back the value.
// After this period, we might wait more frames until a fence is signalled to make sure the rendering is completed.
private readonly _readbackWaitFrames = 4;
// We wait this many frames after *reading back* a measurement until we trigger measure again.
// We could in theory render the measurement pixel immediately, but we wait to make sure
// no pipeline stall happens.
private readonly _measureWaitFrames = 6;
private readonly _texWidth = 1;
private readonly _texHeight = 1;
private readonly _texFormat: number;
private readonly _texType: number;

private _fullscreenTriangle: Mesh;
private _fbo: Framebuffer;
private _resultBuffer: Uint8Array;
private _pbo: WebGLBuffer;

private _measuredError: number = 0; // Result of last measurement
private _updateCount: number = 0;
private _lastReadbackFrame: number = -1000;

get awaitingQuery(): boolean {
return !!this._readbackQueue;
}

// There is never more than one readback waiting
private _readbackQueue: {
frameNumberIssued: number; // Frame number when the data was first computed
sync: WebGLSync;
} = null;

public constructor(painter: Painter) {
const context = painter.context;
const gl = context.gl;

this._texFormat = gl.RGBA;
this._texType = gl.UNSIGNED_BYTE;

const vertexArray = new PosArray();
vertexArray.emplaceBack(-1, -1);
vertexArray.emplaceBack(2, -1);
vertexArray.emplaceBack(-1, 2);
const indexArray = new TriangleIndexArray();
indexArray.emplaceBack(0, 1, 2);

this._fullscreenTriangle = new Mesh(
context.createVertexBuffer(vertexArray, posAttributes.members),
context.createIndexBuffer(indexArray),
SegmentVector.simpleSegment(0, 0, vertexArray.length, indexArray.length)
);

this._resultBuffer = new Uint8Array(4);

context.activeTexture.set(gl.TEXTURE1);

const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, this._texFormat, this._texWidth, this._texHeight, 0, this._texFormat, this._texType, null);

this._fbo = context.createFramebuffer(this._texWidth, this._texHeight, false, false);
this._fbo.colorAttachment.set(texture);

if (gl instanceof WebGL2RenderingContext) {
this._pbo = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._pbo);
gl.bufferData(gl.PIXEL_PACK_BUFFER, 4, gl.STREAM_READ);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
}
}

public destroy(painter: Painter) {
const gl = painter.context.gl;
this._fullscreenTriangle.destroy();
this._fbo.destroy();
gl.deleteBuffer(this._pbo);
this._fullscreenTriangle = null;
this._fbo = null;
this._pbo = null;
this._resultBuffer = null;
}

public updateErrorLoop(painter: Painter, normalizedMercatorY: number, expectedAngleY: number): number {
const currentFrame = this._updateCount;

if (this._readbackQueue) {
// Try to read back if enough frames elapsed. Otherwise do nothing, just wait another frame.
if (currentFrame >= this._readbackQueue.frameNumberIssued + this._readbackWaitFrames) {
// Try to read back - it is possible that this method does nothing, then
// the readback queue will not be cleared and we will retry next frame.
this._tryReadback(painter.context);
}
} else {
if (currentFrame >= this._lastReadbackFrame + this._measureWaitFrames) {
this._renderErrorTexture(painter, normalizedMercatorY, expectedAngleY);
}
}

this._updateCount++;
return this._measuredError;
}

private _bindFramebuffer(context: Context) {
const gl = context.gl;
context.activeTexture.set(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this._fbo.colorAttachment.get());
context.bindFramebuffer.set(this._fbo.framebuffer);
}

private _renderErrorTexture(painter: Painter, input: number, outputExpected: number): void {
const context = painter.context;
const gl = context.gl;

// Update framebuffer contents
this._bindFramebuffer(painter.context);
context.viewport.set([0, 0, this._texWidth, this._texHeight]);
context.clear({color: Color.transparent});

const program = painter.useProgram('projectionErrorMeasurement');

program.draw(context, gl.TRIANGLES,
DepthMode.disabled, StencilMode.disabled,
ColorMode.unblended, CullFaceMode.disabled,
projectionErrorMeasurementUniformValues(input, outputExpected), null, null,
'$clipping', this._fullscreenTriangle.vertexBuffer, this._fullscreenTriangle.indexBuffer,
this._fullscreenTriangle.segments);

if (this._pbo && gl instanceof WebGL2RenderingContext) {
// Read back into PBO
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._pbo);
gl.readBuffer(gl.COLOR_ATTACHMENT0);
gl.readPixels(0, 0, this._texWidth, this._texHeight, this._texFormat, this._texType, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.flush();

this._readbackQueue = {
frameNumberIssued: this._updateCount,
sync,
};
} else {
// Read it back later.
this._readbackQueue = {
frameNumberIssued: this._updateCount,
sync: null,
};
}
}

private _tryReadback(context: Context): void {
const gl = context.gl;

if (this._pbo && this._readbackQueue && gl instanceof WebGL2RenderingContext) {
HarelM marked this conversation as resolved.
Show resolved Hide resolved
// WebGL 2 path
const waitResult = gl.clientWaitSync(this._readbackQueue.sync, 0, 0);

if (waitResult === gl.WAIT_FAILED) {
warnOnce('WebGL2 clientWaitSync failed.');
this._readbackQueue = null;
this._lastReadbackFrame = this._updateCount;
return;
}

if (waitResult === gl.TIMEOUT_EXPIRED) {
return; // Wait one more frame
}

gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._pbo);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, this._resultBuffer, 0, 4);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
} else {
// WebGL1 compatible
this._bindFramebuffer(context);
gl.readPixels(0, 0, this._texWidth, this._texHeight, this._texFormat, this._texType, this._resultBuffer);
}

// If we made it here, _resultBuffer contains the new measurement
this._readbackQueue = null;
this._measuredError = parseRGBA8float(this._resultBuffer);
this._lastReadbackFrame = this._updateCount;
}
}

function parseRGBA8float(buffer: Uint8Array): number {
HarelM marked this conversation as resolved.
Show resolved Hide resolved
let result = 0;
result += buffer[0] / 256.0;
result += buffer[1] / 65536.0;
result += buffer[2] / 16777216.0;
if (buffer[3] < 127.0) {
result = -result;
}
return result / 128.0;
}
38 changes: 37 additions & 1 deletion src/geo/projection/mercator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ import {mat4} from 'gl-matrix';
import {Painter} from '../../render/painter';
import {Transform} from '../transform';
import {ProjectionBase} from './projection_base';
import {UnwrappedTileID} from '../../source/tile_id';
import {CanonicalTileID, UnwrappedTileID} from '../../source/tile_id';
import Point from '@mapbox/point-geometry';
import {Tile} from '../../source/tile';
import {ProjectionData} from '../../render/program/projection_program';
import {pixelsToTileUnits} from '../../source/pixels_to_tile_units';
import {EXTENT} from '../../data/extent';
import {PreparedShader, shaders} from '../../shaders/shaders';
import {Context} from '../../gl/context';
import {Mesh} from '../../render/mesh';
import {PosArray, TriangleIndexArray} from '../../data/array_types.g';
import {SegmentVector} from '../../data/segment';
import posAttributes from '../../data/pos_attributes';

export const MercatorShaderDefine = '#define PROJECTION_MERCATOR';
export const MercatorShaderVariantKey = 'mercator';

export class MercatorProjection implements ProjectionBase {
private _cachedMesh: Mesh = null;

get name(): string {
return 'mercator';
}
Expand All @@ -32,6 +39,11 @@ export class MercatorProjection implements ProjectionBase {
return true;
}

get useSubdivision(): boolean {
// Mercator never uses subdivision.
return false;
}

get shaderVariantName(): string {
return MercatorShaderVariantKey;
}
Expand Down Expand Up @@ -107,6 +119,30 @@ export class MercatorProjection implements ProjectionBase {
translatePosition(transform: Transform, tile: Tile, translate: [number, number], translateAnchor: 'map' | 'viewport'): [number, number] {
return translatePosition(transform, tile, translate, translateAnchor);
}

getMeshFromTileID(context: Context, _: CanonicalTileID, _hasBorder: boolean): Mesh {
if (this._cachedMesh) {
return this._cachedMesh;
}

// Both poles/canonicalTileID and borders are ignored for mercator meshes on purpose.

const tileExtentArray = new PosArray();
tileExtentArray.emplaceBack(0, 0);
tileExtentArray.emplaceBack(EXTENT, 0);
tileExtentArray.emplaceBack(0, EXTENT);
tileExtentArray.emplaceBack(EXTENT, EXTENT);
const tileExtentBuffer = context.createVertexBuffer(tileExtentArray, posAttributes.members);
const tileExtentSegments = SegmentVector.simpleSegment(0, 0, 4, 2);

const quadTriangleIndices = new TriangleIndexArray();
quadTriangleIndices.emplaceBack(1, 0, 2);
quadTriangleIndices.emplaceBack(1, 2, 3);
const quadTriangleIndexBuffer = context.createIndexBuffer(quadTriangleIndices);

this._cachedMesh = new Mesh(tileExtentBuffer, quadTriangleIndexBuffer, tileExtentSegments);
return this._cachedMesh;
}
}

/**
Expand Down
23 changes: 21 additions & 2 deletions src/geo/projection/projection_base.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {mat4} from 'gl-matrix';
import {Painter} from '../../render/painter';
import {Tile} from '../../source/tile';
import {UnwrappedTileID} from '../../source/tile_id';
import {CanonicalTileID, UnwrappedTileID} from '../../source/tile_id';
import {Transform} from '../transform';
import Point from '@mapbox/point-geometry';
import {ProjectionData} from '../../render/program/projection_program';
import {PreparedShader} from '../../shaders/shaders';
import {Context} from '../../gl/context';
import {Mesh} from '../../render/mesh';

/**
* An abstract class the specializations of which are used internally by MapLibre to handle different projections.
Expand Down Expand Up @@ -33,10 +35,18 @@ export interface ProjectionBase {

/**
* @internal
* True if this projection required wrapped copies of the world to be drawn.
* True if this projection requires wrapped copies of the world to be drawn.
*/
get drawWrappedTiles(): boolean;

/**
* @internal
* True if this projection needs to render subdivided geometry.
* Optimized rendering paths for non-subdivided geometry might be used throughout MapLibre.
* The value of this property may change during runtime, for example in globe projection depending on zoom.
*/
get useSubdivision(): boolean;

/**
* Name of the shader projection variant that should be used for this projection.
* Note that this value may change dynamically, for example when globe projection internally transitions to mercator.
Expand Down Expand Up @@ -117,4 +127,13 @@ export interface ProjectionBase {
* Returns a translation in tile units that correctly incorporates the view angle and the *-translate and *-translate-anchor properties.
*/
translatePosition(transform: Transform, tile: Tile, translate: [number, number], translateAnchor: 'map' | 'viewport'): [number, number];

/**
* @internal
* Returns a subdivided mesh for a given canonical tile ID, covering 0..EXTENT range.
* @param context - WebGL context.
* @param canonical - The tile coordinates for which to return a mesh. Meshes for tiles that border the top/bottom mercator edge might include extra geometry for the north/south pole.
* @param hasBorder - When true, the mesh will also include a small border beyond the 0..EXTENT range.
*/
getMeshFromTileID(context: Context, canonical: CanonicalTileID, hasBorder: boolean): Mesh;
}
6 changes: 3 additions & 3 deletions src/render/draw_fill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ jest.mock('../symbol/projection');
describe('drawFill', () => {
test('should call programConfiguration.setConstantPatternPositions for transitioning fill-pattern', () => {

const painterMock: Painter = constructMockPainer();
const painterMock: Painter = constructMockPainter();
const layer: FillStyleLayer = constructMockLayer();

const programMock = new Program(null as any, null as any, null as any, null as any, null as any, null as any, null as any);
const programMock = new Program(null as any, null as any, null as any, null as any, null as any, null as any, null as any, null as any);
(painterMock.useProgram as jest.Mock).mockReturnValue(programMock);

const mockTile = constructMockTile(layer);
Expand Down Expand Up @@ -73,7 +73,7 @@ describe('drawFill', () => {
return layer;
}

function constructMockPainer(): Painter {
function constructMockPainter(): Painter {
const painterMock = new Painter(null as any, null as any);
painterMock.context = {
gl: {},
Expand Down