From 0a4a25a3d30825cae34bd678a01828708be6f244 Mon Sep 17 00:00:00 2001 From: felixpalmer Date: Wed, 11 Dec 2024 11:16:30 +0100 Subject: [PATCH] CARTO: Fix seams between tiles in RasterTileLayer (#9286) --- .../carto/src/layers/post-process-utils.ts | 31 ++++++++++++++++++- .../src/layers/raster-layer-vertex.glsl.ts | 18 ++++++----- modules/carto/src/layers/raster-layer.ts | 17 ++++++++-- modules/carto/src/layers/raster-tile-layer.ts | 23 ++++++++++++-- 4 files changed, 76 insertions(+), 13 deletions(-) diff --git a/modules/carto/src/layers/post-process-utils.ts b/modules/carto/src/layers/post-process-utils.ts index dd09f728566..5cde969c591 100644 --- a/modules/carto/src/layers/post-process-utils.ts +++ b/modules/carto/src/layers/post-process-utils.ts @@ -3,7 +3,9 @@ // Copyright (c) vis.gl contributors import {Framebuffer, TextureProps} from '@luma.gl/core'; +import type {ShaderPass} from '@luma.gl/shadertools'; import { + _ConstructorOf, CompositeLayer, Layer, LayerContext, @@ -63,8 +65,10 @@ class DrawCallbackLayer extends Layer { * Resulting layer must be used as a sublayer of a layer created * with `PostProcessModifier` */ -export function RTTModifier(BaseLayer) { +export function RTTModifier>(BaseLayer: T): T { + // @ts-expect-error initializeState is abstract return class RTTLayer extends BaseLayer { + // @ts-expect-error typescript doesn't see static property static layerName = `RTT-${BaseLayer.layerName}`; draw(this: RTTLayer, opts: any) { @@ -181,5 +185,30 @@ export function PostProcessModifier { + fbo.destroy(); + }); + this.internalState.renderBuffers = null; + this.internalState.postProcess.cleanup(); + } }; } + +const fs = /* glsl */ `\ +vec4 copy_filterColor_ext(vec4 color, vec2 texSize, vec2 texCoord) { + return color; +} +`; + +/** + * Copy + * Simple module that just copies input color to output + */ +export const copy = { + name: 'copy', + fs, + getUniforms: () => ({}), + passes: [{filter: true}] +} as const satisfies ShaderPass<{}>; diff --git a/modules/carto/src/layers/raster-layer-vertex.glsl.ts b/modules/carto/src/layers/raster-layer-vertex.glsl.ts index f4b2788c9f4..40fc3ae310b 100644 --- a/modules/carto/src/layers/raster-layer-vertex.glsl.ts +++ b/modules/carto/src/layers/raster-layer-vertex.glsl.ts @@ -23,12 +23,14 @@ out vec4 position_commonspace; void main(void) { // Rather than positioning using attribute, layout pixel grid using gl_InstanceID - vec2 common_position = column.offset.xy; + vec2 tileOrigin = column.offset.xy; float scale = column.widthScale; // Re-use widthScale prop to pass cell scale int yIndex = - (gl_InstanceID / BLOCK_WIDTH); int xIndex = gl_InstanceID + (yIndex * BLOCK_WIDTH); - common_position += scale * vec2(float(xIndex), float(yIndex - 1)); + + // Avoid precision issues by applying 0.5 offset here, rather than when laying out vertices + vec2 cellCenter = scale * vec2(float(xIndex) + 0.5, float(yIndex) - 0.5); vec4 color = column.isStroke ? instanceLineColors : instanceFillColors; @@ -39,7 +41,7 @@ void main(void) { // Get position directly from quadbin, rather than projecting // Important to set geometry.position before using project_ methods below // as geometry.worldPosition is not set (we don't know our lat/long) - geometry.position = vec4(common_position, 0.0, 1.0); + geometry.position = vec4(tileOrigin + cellCenter, 0.0, 1.0); if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET) { geometry.position.xyz -= project.commonOrigin; } @@ -63,12 +65,12 @@ void main(void) { geometry.pickingColor = instancePickingColors; - // project center of column - vec2 offset = (vec2(0.5) + positions.xy * strokeOffsetRatio) * cellWidth * shouldRender; - vec3 pos = vec3(offset, project_size(elevation)); - DECKGL_FILTER_SIZE(pos, geometry); + // Cell coordinates centered on origin + vec2 base = positions.xy * scale * strokeOffsetRatio * column.coverage * shouldRender; + vec3 cell = vec3(base, project_size(elevation)); + DECKGL_FILTER_SIZE(cell, geometry); - geometry.position.xyz += pos; + geometry.position.xyz += cell; gl_Position = project_common_position_to_clipspace(geometry.position); geometry.normal = project_normal(normals); diff --git a/modules/carto/src/layers/raster-layer.ts b/modules/carto/src/layers/raster-layer.ts index 074dd4987da..7953409be71 100644 --- a/modules/carto/src/layers/raster-layer.ts +++ b/modules/carto/src/layers/raster-layer.ts @@ -16,6 +16,7 @@ import {quadbinToOffset} from './quadbin-utils'; import {Raster} from './schema/carto-raster-tile-loader'; import vs from './raster-layer-vertex.glsl'; import {createBinaryProxy} from '../utils'; +import {RTTModifier} from './post-process-utils'; const defaultProps: DefaultProps = { ...ColumnLayer.defaultProps, @@ -30,7 +31,8 @@ const defaultProps: DefaultProps = { }; // Modified ColumnLayer with custom vertex shader -class RasterColumnLayer extends ColumnLayer { +// Use RTT to avoid inter-tile seams +class RasterColumnLayer extends RTTModifier(ColumnLayer) { static layerName = 'RasterColumnLayer'; getShaders() { @@ -143,7 +145,18 @@ export default class RasterLayer extends Composite offset, lineWidthScale, // Re-use widthScale prop to pass cell scale, highlightedObjectIndex, - highlightColor + highlightColor, + + // RTT requires blending otherwise opacity < 1 blends with black + // render target + parameters: { + blendColorSrcFactor: 'one', + blendAlphaSrcFactor: 'one', + blendColorDstFactor: 'zero', + blendAlphaDstFactor: 'zero', + blendColorOperation: 'add', + blendAlphaOperation: 'add' + } } ); } diff --git a/modules/carto/src/layers/raster-tile-layer.ts b/modules/carto/src/layers/raster-tile-layer.ts index d4499c69a6c..d3155a6897e 100644 --- a/modules/carto/src/layers/raster-tile-layer.ts +++ b/modules/carto/src/layers/raster-tile-layer.ts @@ -2,13 +2,21 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {CompositeLayer, CompositeLayerProps, DefaultProps, Layer, LayersList} from '@deck.gl/core'; +import { + CompositeLayer, + CompositeLayerProps, + DefaultProps, + FilterContext, + Layer, + LayersList +} from '@deck.gl/core'; import RasterLayer, {RasterLayerProps} from './raster-layer'; import QuadbinTileset2D from './quadbin-tileset-2d'; import type {TilejsonResult} from '@carto/api-client'; import {injectAccessToken, TilejsonPropType} from './utils'; import {DEFAULT_TILE_SIZE} from '../constants'; import {TileLayer, TileLayerProps} from '@deck.gl/geo-layers'; +import {copy, PostProcessModifier} from './post-process-utils'; export const renderSubLayers = props => { const tileIndex = props.tile?.index?.q; @@ -18,6 +26,7 @@ export const renderSubLayers = props => { const defaultProps: DefaultProps = { data: TilejsonPropType, + refinementStrategy: 'no-overlap', tileSize: DEFAULT_TILE_SIZE }; @@ -31,6 +40,16 @@ type _RasterTileLayerProps = Omit, 'data'> & data: null | TilejsonResult | Promise; }; +class PostProcessTileLayer extends PostProcessModifier(TileLayer, copy) { + filterSubLayer(context: FilterContext) { + // Handle DrawCallbackLayer + const {tile} = (context.layer as Layer<{tile: any}>).props; + if (!tile) return true; + + return super.filterSubLayer(context); + } +} + export default class RasterTileLayer< DataT = any, ExtraProps extends {} = {} @@ -50,7 +69,7 @@ export default class RasterTileLayer< if (!tileJSON) return null; const {tiles: data, minzoom: minZoom, maxzoom: maxZoom} = tileJSON; - const SubLayerClass = this.getSubLayerClass('tile', TileLayer); + const SubLayerClass = this.getSubLayerClass('tile', PostProcessTileLayer); return new SubLayerClass(this.props, { id: `raster-tile-layer-${this.props.id}`, data,