From 3ec39018a919d99eb63bc1f3da32ecbb940fa011 Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:37:48 -0400 Subject: [PATCH 1/8] polygon --- packages/engine/Source/Core/VectorPipeline.js | 519 +++++++++++++++++- packages/engine/Source/Core/VectorProvider.js | 48 +- .../Source/Scene/GlobeSurfaceTileProvider.js | 27 + packages/engine/Source/Shaders/GlobeFS.glsl | 1 + .../engine/Source/Shaders/VectorCommon.glsl | 81 +++ .../engine/Specs/Core/VectorProviderSpec.js | 176 ++++++ 6 files changed, 820 insertions(+), 32 deletions(-) diff --git a/packages/engine/Source/Core/VectorPipeline.js b/packages/engine/Source/Core/VectorPipeline.js index 52523e47d58..9398fc28d20 100644 --- a/packages/engine/Source/Core/VectorPipeline.js +++ b/packages/engine/Source/Core/VectorPipeline.js @@ -3,6 +3,8 @@ import PixelDatatype from "../Renderer/PixelDatatype.js"; import Sampler from "../Renderer/Sampler.js"; import Texture from "../Renderer/Texture.js"; +import BufferPolygon from "../Scene/BufferPolygon.js"; +import BufferPolygonMaterial from "../Scene/BufferPolygonMaterial.js"; import BufferPolyline from "../Scene/BufferPolyline.js"; import BufferPolylineMaterial from "../Scene/BufferPolylineMaterial.js"; import Cartesian2 from "./Cartesian2.js"; @@ -17,6 +19,7 @@ import Rectangle from "./Rectangle.js"; /** @import BufferPrimitive from "../Scene/BufferPrimitive.js"; */ /** @import BufferPrimitiveCollection from "../Scene/BufferPrimitiveCollection.js"; */ +/** @import BufferPolygonCollection from "../Scene/BufferPolygonCollection.js"; */ /** @import BufferPolylineCollection from "../Scene/BufferPolylineCollection.js"; */ /** @import Context from "../Renderer/Context.js"; */ /** @import Ellipsoid from "./Ellipsoid.js"; */ @@ -25,8 +28,14 @@ import Rectangle from "./Rectangle.js"; const GRID_TARGET_SEGMENTS_PER_CELL = 16; const GRID_NEIGHBOR_PADDING_SCALE = 0.35; +// Grid cells expand by this UV epsilon when clipping polygon rings, so every +// fragment mapping to a cell lies strictly inside the cell's clipped loops. +const POLYGON_CELL_CLIP_EPSILON = 1.0e-5; + const scratchPolyline = new BufferPolyline(); const scratchPolylineMaterial = new BufferPolylineMaterial(); +const scratchPolygon = new BufferPolygon(); +const scratchPolygonMaterial = new BufferPolygonMaterial(); const scratchLocalPosition = new Cartesian3(); const scratchWorldPosition = new Cartesian3(); const scratchCartographic = new Cartographic(); @@ -40,11 +49,13 @@ const scratchSegmentEnd = new Cartesian2(); * * @property {boolean} show Whether this vector data should be rendered. * - * Stage 1: Collect vector segments intersecting tile. + * Stage 1: Collect vector segments and polygon rings intersecting tile. * @property {number[][]} [segments] * @property {number[]} [segmentPrimitiveIndices] Index per segment, mapping to material for the segment. - * @property {Uint8Array[]} [widths] Segment widths, by primitive index. - * @property {Uint8Array[]} [colors] Segment colors, by primitive index. + * @property {Float64Array[]} [polygonRings] Tile-clipped polygon rings as flat [x0, y0, x1, y1, ...] in tile UV space. + * @property {number[]} [polygonRingPrimitiveIndices] Index per ring, mapping to material for the ring. + * @property {Uint8Array[]} [widths] Primitive widths, by primitive index. + * @property {Uint8Array[]} [colors] Primitive colors, by primitive index. * @property {number} [primitiveCount] Number of vector primitives in tile. * * Stage 2: Build CPU grid structures. @@ -53,19 +64,27 @@ const scratchSegmentEnd = new Cartesian2(); * @property {number} [segmentTextureHeight] Height of the segment texture, in texels. * @property {Float32Array} [segmentPrimitiveIndicesTexels] Index per segment, mapping to material for the segment. * @property {Uint32Array} [gridCellIndices] Grid header [gridWidth, gridHeight, ...per-cell end offsets]. + * @property {Float32Array} [polygonEdgeTexels] Packed RGBA polygon ring edges (ax, ay, bx, by), clipped per grid cell, -1 filled. + * @property {number} [polygonEdgeTextureWidth] Width of the polygon edge texture, in texels. + * @property {number} [polygonEdgeTextureHeight] Height of the polygon edge texture, in texels. + * @property {Float32Array} [polygonEdgePrimitiveIndicesTexels] Index per polygon edge, mapping to material for the edge. + * @property {Uint32Array} [polygonGridCellIndices] Polygon grid header [gridWidth, gridHeight, ...per-cell end offsets]. * * Stage 3: Build GPU texture resources, uploaded lazily at draw time. * @property {Texture} [segmentTexture] GPU texture of segmentTexels. * @property {Texture} [segmentPrimitiveIndicesTexture] GPU texture of primitive indices per segment. - * @property {Texture} [widthTexture] GPU texture of segment widths, by primitive index. - * @property {Texture} [colorTexture] GPU texture of segment colors, by primitive index. + * @property {Texture} [widthTexture] GPU texture of primitive widths, by primitive index. + * @property {Texture} [colorTexture] GPU texture of primitive colors, by primitive index. * @property {Texture} [gridCellIndicesTexture] GPU texture of gridCellIndices. + * @property {Texture} [polygonEdgeTexture] GPU texture of polygonEdgeTexels. + * @property {Texture} [polygonEdgePrimitiveIndicesTexture] GPU texture of primitive indices per polygon edge. + * @property {Texture} [polygonGridCellIndicesTexture] GPU texture of polygonGridCellIndices. * * @private */ /** - * Snapshot of a polyline collection — projected vertex positions and + * Snapshot of a vector collection — projected vertex positions and * per-primitive material properties — extracted in a single pass so the * collection can be marked clean immediately afterward. * @@ -74,7 +93,7 @@ const scratchSegmentEnd = new Cartesian2(); * @property {number} version State of `collection._version` at time data was last updated. * @property {Rectangle} rectangle * @property {Float64Array} positions Collection positions, projected to the ellipsoid as [lng, lat] in radians. - * @property {Uint8Array} widths Primitive widths, by primitive index. + * @property {Uint8Array} widths Primitive widths, by primitive index. Zero-filled for polygon collections. * @property {Uint8Array} colors Primitive colors, by primitive index. * * @private @@ -316,23 +335,272 @@ class VectorPipeline { } /** - * @param {Context} context + * @param {BufferPolygonCollection} collection + * @param {TilingScheme} tilingScheme + * @param {VectorCollectionData} [result] + * @returns {VectorCollectionData} + */ + static packPolygonCollectionData(collection, tilingScheme, result) { + if ( + defined(result) && + collection._dirtyCount === 0 && + collection._version === result.version + ) { + return result; + } + + const primitiveCount = collection.primitiveCount; + const boundingVolume = collection.boundingVolume; + const ellipsoid = tilingScheme.ellipsoid; + + const rectangle = Rectangle.fromBoundingSphere(boundingVolume, ellipsoid); + const positions = _getProjectedPositions(collection, ellipsoid); + + // Widths are unused by polygon fills; zero-filled so polygons share the + // primitive index space (and width/color textures) with polylines. + const widths = new Uint8Array(primitiveCount); + const colors = new Uint8Array(primitiveCount * 4); + + for (let i = 0; i < primitiveCount; i++) { + const polygon = /** @type {BufferPolygon} */ ( + collection.get(i, scratchPolygon) + ); + + // Append materials unconditionally, to simplify indexing and updates. + const polygonMaterial = /** @type {BufferPolygonMaterial} */ ( + polygon.getMaterial(scratchPolygonMaterial) + ); + + colors[i * 4] = Color.floatToByte(polygonMaterial.color.red); + colors[i * 4 + 1] = Color.floatToByte(polygonMaterial.color.green); + colors[i * 4 + 2] = Color.floatToByte(polygonMaterial.color.blue); + colors[i * 4 + 3] = Color.floatToByte(polygonMaterial.color.alpha); + } + + return Object.assign( + result ?? {}, + /** @type {VectorCollectionData} */ ({ + version: collection._version, + rectangle: rectangle, + positions: positions, + widths: widths, + colors: colors, + }), + ); + } + + /** + * Projects all visible polygon rings (outer rings and holes) in a collection + * into tile-local UV space, clipped to the tile, appending each surviving + * ring as a flat [x0, y0, x1, y1, ...] closed loop. + * + * @param {BufferPolygonCollection} collection + * @param {VectorCollectionData} collectionData + * @param {Rectangle} rectangle + * @param {number} width * @param {VectorTileData} result */ - static packPolylineTextures(context, result) { - result.segmentTexture = new Texture({ - context, - pixelFormat: PixelFormat.RGBA, - pixelDatatype: PixelDatatype.FLOAT, - source: { - width: result.segmentTextureWidth, - height: result.segmentTextureHeight, - arrayBufferView: result.segmentTexels, - }, - sampler: Sampler.NEAREST, - flipY: false, - }); + static packPolygonRings( + collection, + collectionData, + rectangle, + width, + result, + ) { + result.polygonRings ??= []; + result.polygonRingPrimitiveIndices ??= []; + result.widths ??= []; + result.colors ??= []; + result.primitiveCount ??= 0; + + const primitiveCount = collection.primitiveCount; + const positions = collectionData.positions; + + for (let i = 0; i < primitiveCount; i++) { + const polygon = /** @type {BufferPolygon} */ ( + collection.get(i, scratchPolygon) + ); + if (!polygon.show) { + continue; + } + + const vertexOffset = polygon.vertexOffset; + const vertexCount = polygon.vertexCount; + const holes = polygon.getHoles(); + + // Ring r spans [ringStart, ringEnd): the outer ring, then each hole. + // The shader's even-odd test makes hole rings cancel enclosing coverage. + for (let r = 0; r <= holes.length; r++) { + const ringStart = r === 0 ? 0 : holes[r - 1]; + const ringEnd = r === holes.length ? vertexCount : holes[r]; + const ringVertexCount = ringEnd - ringStart; + if (ringVertexCount < 3) { + continue; + } + + const ringUv = _projectRingToTileUv( + positions, + vertexOffset + ringStart, + ringVertexCount, + rectangle, + width, + ); + + // Clip to the tile plus the polyline clip margin. A ring enclosing + // the whole tile clips to the tile rectangle itself, so tiles + // interior to a large polygon remain covered. + const clippedCount = _clipRingToRect( + ringUv, + ringVertexCount, + -CesiumMath.EPSILON3, + 1.0 + CesiumMath.EPSILON3, + -CesiumMath.EPSILON3, + 1.0 + CesiumMath.EPSILON3, + ); + if (clippedCount < 3) { + continue; + } + + result.polygonRings.push(scratchClipB.slice(0, clippedCount * 2)); + result.polygonRingPrimitiveIndices.push(result.primitiveCount + i); + } + } + + // Append materials unconditionally, to simplify indexing and updates. + result.widths.push(collectionData.widths); + result.colors.push(collectionData.colors); + + result.primitiveCount += primitiveCount; + } + + /** + * Packs UV-space polygon rings into a grid-indexed edge lookup. Each ring + * is clipped to every grid cell it overlaps — unlike polyline segments, + * which are only assigned to cells — so a cell's edges form closed loops + * and a fragment can evaluate even-odd coverage from its own cell alone. + * + * @param {VectorTileData} result + */ + static packPolygonGrid(result) { + const rings = result.polygonRings; + const ringPrimitiveIndices = result.polygonRingPrimitiveIndices; + + let ringEdgeCount = 0; + for (let i = 0; i < rings.length; i++) { + ringEdgeCount += rings[i].length / 2; + } + + const gridSize = Math.max( + 1, + Math.ceil(Math.sqrt(ringEdgeCount / GRID_TARGET_SEGMENTS_PER_CELL)), + ); + + /** @type {number[][]} Per-cell packed edges [ax, ay, bx, by, primitiveIndex, ...]. */ + const grid = new Array(gridSize * gridSize); + for (let i = 0; i < grid.length; i++) { + grid[i] = []; + } + // Rings are appended in primitive order, so each cell's edge list stays + // grouped by primitive; the shader resolves parity per group. + let packedEdgeCount = 0; + for (let r = 0; r < rings.length; r++) { + const ring = rings[r]; + const primitiveIndex = ringPrimitiveIndices[r]; + const ringVertexCount = ring.length / 2; + + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + for (let k = 0; k < ringVertexCount; k++) { + minX = Math.min(minX, ring[k * 2]); + maxX = Math.max(maxX, ring[k * 2]); + minY = Math.min(minY, ring[k * 2 + 1]); + maxY = Math.max(maxY, ring[k * 2 + 1]); + } + + const startCellX = _clampCellIndex(Math.floor(minX * gridSize), gridSize); + const endCellX = _clampCellIndex(Math.floor(maxX * gridSize), gridSize); + const startCellY = _clampCellIndex(Math.floor(minY * gridSize), gridSize); + const endCellY = _clampCellIndex(Math.floor(maxY * gridSize), gridSize); + + for (let y = startCellY; y <= endCellY; y++) { + for (let x = startCellX; x <= endCellX; x++) { + // Expand the clip rectangle slightly so fragments at shared cell + // boundaries fall strictly inside the clipped loops. + const clippedCount = _clipRingToRect( + ring, + ringVertexCount, + x / gridSize - POLYGON_CELL_CLIP_EPSILON, + (x + 1) / gridSize + POLYGON_CELL_CLIP_EPSILON, + y / gridSize - POLYGON_CELL_CLIP_EPSILON, + (y + 1) / gridSize + POLYGON_CELL_CLIP_EPSILON, + ); + if (clippedCount < 3) { + continue; + } + + const cell = grid[y * gridSize + x]; + for (let k = 0; k < clippedCount; k++) { + const k2 = (k + 1) % clippedCount; + const ax = scratchClipB[k * 2]; + const ay = scratchClipB[k * 2 + 1]; + const bx = scratchClipB[k2 * 2]; + const by = scratchClipB[k2 * 2 + 1]; + // Horizontal edges never toggle parity against the shader's + // horizontal ray; clipping produces many along cell bounds. + if (ay === by) { + continue; + } + cell.push(ax, ay, bx, by, primitiveIndex); + packedEdgeCount++; + } + } + } + } + + const [textureWidth, textureHeight] = _nextPowerOfTwoSize(packedEdgeCount); + const capacity = textureWidth * textureHeight; + + const edgeTexels = new Float32Array(capacity * 4).fill(-1.0); + const edgePrimitiveIndicesTexels = new Float32Array(capacity).fill(-1.0); + + const gridCellIndices = new Uint32Array(grid.length + 2); + gridCellIndices[0] = gridSize; + gridCellIndices[1] = gridSize; + + let offset = 0; + for (let i = 0; i < grid.length; i++) { + const cellEdges = grid[i]; + for (let j = 0; j < cellEdges.length; j += 5) { + edgeTexels[offset * 4] = cellEdges[j]; // R + edgeTexels[offset * 4 + 1] = cellEdges[j + 1]; // G + edgeTexels[offset * 4 + 2] = cellEdges[j + 2]; // B + edgeTexels[offset * 4 + 3] = cellEdges[j + 3]; // A + + edgePrimitiveIndicesTexels[offset] = cellEdges[j + 4]; + + offset++; + } + gridCellIndices[i + 2] = offset; + } + + result.polygonEdgeTexels = edgeTexels; + result.polygonEdgeTextureWidth = textureWidth; + result.polygonEdgeTextureHeight = textureHeight; + result.polygonEdgePrimitiveIndicesTexels = edgePrimitiveIndicesTexels; + result.polygonGridCellIndices = gridCellIndices; + } + + /** + * Creates the width and color textures indexed by the primitive index space + * shared between polylines and polygons. + * + * @param {Context} context + * @param {VectorTileData} result + */ + static packPrimitiveTextures(context, result) { const [primTextureWidth, primTextureHeight] = _nextPowerOfTwoSize( result.primitiveCount, ); @@ -370,6 +638,25 @@ class VectorPipeline { sampler: Sampler.NEAREST, flipY: false, }); + } + + /** + * @param {Context} context + * @param {VectorTileData} result + */ + static packPolylineTextures(context, result) { + result.segmentTexture = new Texture({ + context, + pixelFormat: PixelFormat.RGBA, + pixelDatatype: PixelDatatype.FLOAT, + source: { + width: result.segmentTextureWidth, + height: result.segmentTextureHeight, + arrayBufferView: result.segmentTexels, + }, + sampler: Sampler.NEAREST, + flipY: false, + }); result.segmentPrimitiveIndicesTexture = new Texture({ context, @@ -398,6 +685,51 @@ class VectorPipeline { }); } + /** + * @param {Context} context + * @param {VectorTileData} result + */ + static packPolygonTextures(context, result) { + result.polygonEdgeTexture = new Texture({ + context, + pixelFormat: PixelFormat.RGBA, + pixelDatatype: PixelDatatype.FLOAT, + source: { + width: result.polygonEdgeTextureWidth, + height: result.polygonEdgeTextureHeight, + arrayBufferView: result.polygonEdgeTexels, + }, + sampler: Sampler.NEAREST, + flipY: false, + }); + + result.polygonEdgePrimitiveIndicesTexture = new Texture({ + context, + pixelFormat: PixelFormat.RED, + pixelDatatype: PixelDatatype.FLOAT, + source: { + width: result.polygonEdgeTextureWidth, + height: result.polygonEdgeTextureHeight, + arrayBufferView: result.polygonEdgePrimitiveIndicesTexels, + }, + sampler: Sampler.NEAREST, + flipY: false, + }); + + result.polygonGridCellIndicesTexture = new Texture({ + context: context, + pixelFormat: PixelFormat.RED, + pixelDatatype: PixelDatatype.FLOAT, + source: { + width: result.polygonGridCellIndices.length, + height: 1, + arrayBufferView: new Float32Array(result.polygonGridCellIndices), + }, + sampler: Sampler.NEAREST, + flipY: false, + }); + } + /** * @param {VectorTileData} data */ @@ -407,6 +739,9 @@ class VectorPipeline { data.colorTexture?.destroy(); data.segmentPrimitiveIndicesTexture?.destroy(); data.gridCellIndicesTexture?.destroy(); + data.polygonEdgeTexture?.destroy(); + data.polygonEdgePrimitiveIndicesTexture?.destroy(); + data.polygonGridCellIndicesTexture?.destroy(); } } @@ -517,6 +852,148 @@ function _clampCellIndex(index, gridSize) { return Math.max(0, Math.min(gridSize - 1, index)); } +// Growable module scratch for ring projection and Sutherland-Hodgman clipping. +let scratchRingUv = new Float64Array(512); +let scratchClipA = new Float64Array(512); +let scratchClipB = new Float64Array(512); + +/** + * Projects a polygon ring ([lng, lat] radian pairs from `positions`) into the + * tile's [0,1]^2 UV domain. Longitudes are sequentially unwrapped so an + * antimeridian-crossing ring stays continuous, then the whole ring is shifted + * to the 2π frame nearest the tile center (matching the polyline projection). + * Returns a module scratch valid until the next call. + * + * Only supporting geographic tiling scheme (UV maps linearly to lon/lat) + * + * @param {Float64Array} positions Projected collection positions ([lng, lat] radians). + * @param {number} vertexStart Index of the ring's first vertex. + * @param {number} vertexCount Number of vertices in the ring. + * @param {Rectangle} rectangle + * @param {number} width + * @returns {Float64Array} Flat [u0, v0, u1, v1, ...] ring coordinates. + * @private + */ +function _projectRingToTileUv( + positions, + vertexStart, + vertexCount, + rectangle, + width, +) { + if (scratchRingUv.length < vertexCount * 2) { + scratchRingUv = new Float64Array(vertexCount * 4); + } + const result = scratchRingUv; + const height = rectangle.north - rectangle.south; + + let previousLon = positions[vertexStart * 2]; + result[0] = previousLon; + result[1] = positions[vertexStart * 2 + 1]; + for (let i = 1; i < vertexCount; i++) { + const lon = + previousLon + + CesiumMath.negativePiToPi(positions[(vertexStart + i) * 2] - previousLon); + result[i * 2] = lon; + result[i * 2 + 1] = positions[(vertexStart + i) * 2 + 1]; + previousLon = lon; + } + + const center = rectangle.west + width * 0.5; + const lon0 = result[0]; + const shift = center + CesiumMath.negativePiToPi(lon0 - center) - lon0; + + for (let i = 0; i < vertexCount; i++) { + result[i * 2] = (result[i * 2] + shift - rectangle.west) / width; + result[i * 2 + 1] = (result[i * 2 + 1] - rectangle.south) / height; + } + + return result; +} + +/** + * Clips a closed ring to an axis-aligned rectangle with Sutherland-Hodgman. + * Returns the clipped vertex count; the clipped ring is left in + * `scratchClipB` (flat [x0, y0, ...]), valid until the next call. Returns 0 + * when the ring is entirely outside. + * + * @param {Float64Array} ring Flat [x0, y0, x1, y1, ...] ring coordinates. + * @param {number} vertexCount + * @param {number} minX + * @param {number} maxX + * @param {number} minY + * @param {number} maxY + * @returns {number} + * @private + */ +function _clipRingToRect(ring, vertexCount, minX, maxX, minY, maxY) { + let input = ring; + let inputCount = vertexCount; + + // axis (0 = x, 1 = y), keep side (1 = keep >= limit, -1 = keep <= limit). + for (let plane = 0; plane < 4; plane++) { + const axis = plane >> 1; + const sign = (plane & 1) === 0 ? 1.0 : -1.0; + const limit = + plane === 0 ? minX : plane === 1 ? maxX : plane === 2 ? minY : maxY; + + // Each input vertex emits at most 2 output vertices. + const requiredLength = inputCount * 4 + 4; + let output = (plane & 1) === 0 ? scratchClipA : scratchClipB; + if (output.length < requiredLength) { + output = new Float64Array(requiredLength * 2); + if ((plane & 1) === 0) { + scratchClipA = output; + } else { + scratchClipB = output; + } + } + + let outputCount = 0; + let prevX = input[(inputCount - 1) * 2]; + let prevY = input[(inputCount - 1) * 2 + 1]; + let prevInside = sign * ((axis === 0 ? prevX : prevY) - limit) >= 0.0; + + for (let i = 0; i < inputCount; i++) { + const x = input[i * 2]; + const y = input[i * 2 + 1]; + const inside = sign * ((axis === 0 ? x : y) - limit) >= 0.0; + + if (inside !== prevInside) { + // Pin the clipped coordinate exactly to the plane so boundary edges + // stay axis-aligned. + const t = + axis === 0 + ? (limit - prevX) / (x - prevX) + : (limit - prevY) / (y - prevY); + output[outputCount * 2] = axis === 0 ? limit : prevX + t * (x - prevX); + output[outputCount * 2 + 1] = + axis === 0 ? prevY + t * (y - prevY) : limit; + outputCount++; + } + if (inside) { + output[outputCount * 2] = x; + output[outputCount * 2 + 1] = y; + outputCount++; + } + + prevX = x; + prevY = y; + prevInside = inside; + } + + if (outputCount === 0) { + return 0; + } + + input = output; + inputCount = outputCount; + } + + // Four passes always end in scratchClipB (ring → A → B → A → B). + return inputCount; +} + /** * @param {number} count * @returns {number[]} diff --git a/packages/engine/Source/Core/VectorProvider.js b/packages/engine/Source/Core/VectorProvider.js index 258203a52ce..abe6d4fc8f0 100644 --- a/packages/engine/Source/Core/VectorProvider.js +++ b/packages/engine/Source/Core/VectorProvider.js @@ -1,5 +1,6 @@ // @ts-check +import BufferPolygonCollection from "../Scene/BufferPolygonCollection.js"; import BufferPolylineCollection from "../Scene/BufferPolylineCollection.js"; import Rectangle from "./Rectangle.js"; import defined from "./defined.js"; @@ -195,7 +196,10 @@ class VectorProvider { } if (collection instanceof BufferPolylineCollection) { - const collectionData = this._getPolylineDataCached(collection); + const collectionData = this._getCollectionDataCached( + collection, + VectorPipeline.packPolylineCollectionData, + ); VectorPipeline.packPolylineSegments( collection, collectionData, @@ -203,16 +207,41 @@ class VectorProvider { width, result, ); + } else if (collection instanceof BufferPolygonCollection) { + const collectionData = this._getCollectionDataCached( + collection, + VectorPipeline.packPolygonCollectionData, + ); + VectorPipeline.packPolygonRings( + collection, + collectionData, + tileRectangle, + width, + result, + ); } } - if (!defined(result.segments) || result.segments.length === 0) { + const hasPolylines = defined(result.segments) && result.segments.length > 0; + const hasPolygons = + defined(result.polygonRings) && result.polygonRings.length > 0; + + if (!hasPolylines && !hasPolygons) { result.show = false; return result; } - VectorPipeline.packPolylineGrid(result); - VectorPipeline.packPolylineTextures(context, result); + if (hasPolylines) { + VectorPipeline.packPolylineGrid(result); + VectorPipeline.packPolylineTextures(context, result); + } + + if (hasPolygons) { + VectorPipeline.packPolygonGrid(result); + VectorPipeline.packPolygonTextures(context, result); + } + + VectorPipeline.packPrimitiveTextures(context, result); return result; } @@ -290,11 +319,12 @@ class VectorProvider { * re-extracted when the collection has changed. The collection is marked * clean only after everything has been read back. * - * @param {BufferPolylineCollection} collection + * @param {BufferPrimitiveCollection} collection + * @param {(collection: *, tilingScheme: TilingScheme, result?: VectorCollectionData) => VectorCollectionData} packCollectionData * @returns {VectorCollectionData} * @private */ - _getPolylineDataCached(collection) { + _getCollectionDataCached(collection, packCollectionData) { const cache = this._collectionDataCache.get(collection); const dirty = collection._dirtyCount > 0; const outdated = cache?.version !== collection._version; @@ -303,11 +333,7 @@ class VectorProvider { return cache; } - const data = VectorPipeline.packPolylineCollectionData( - collection, - this._tilingScheme, - cache, - ); + const data = packCollectionData(collection, this._tilingScheme, cache); // If dirty, the version increments +1 when marked clean below. data.version = collection._version + (dirty ? 1 : 0); diff --git a/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js b/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js index 538e44e3a91..f97cc62e24d 100644 --- a/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js +++ b/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js @@ -1984,6 +1984,24 @@ function createTileUniformMap(frameState, globeSurfaceTileProvider) { frameState.context.defaultTexture ); }, + u_vectorPolygonEdgeTexture: function () { + return ( + this.properties.vectorPolygonEdgeTexture ?? + frameState.context.defaultTexture + ); + }, + u_vectorPolygonEdgePrimitiveIndicesTexture: function () { + return ( + this.properties.vectorPolygonEdgePrimitiveIndicesTexture ?? + frameState.context.defaultTexture + ); + }, + u_vectorPolygonGridCellIndicesTexture: function () { + return ( + this.properties.vectorPolygonGridCellIndicesTexture ?? + frameState.context.defaultTexture + ); + }, // make a separate object so that changes to the properties are seen on // derived commands that combine another uniform map with this one. @@ -2052,6 +2070,9 @@ function createTileUniformMap(frameState, globeSurfaceTileProvider) { vectorColorTexture: undefined, vectorSegmentPrimitiveIndicesTexture: undefined, vectorGridCellIndicesTexture: undefined, + vectorPolygonEdgeTexture: undefined, + vectorPolygonEdgePrimitiveIndicesTexture: undefined, + vectorPolygonGridCellIndicesTexture: undefined, }, }; @@ -3001,6 +3022,12 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) { vectorData.segmentPrimitiveIndicesTexture; uniformMapProperties.vectorGridCellIndicesTexture = vectorData.gridCellIndicesTexture; + uniformMapProperties.vectorPolygonEdgeTexture = + vectorData.polygonEdgeTexture; + uniformMapProperties.vectorPolygonEdgePrimitiveIndicesTexture = + vectorData.polygonEdgePrimitiveIndicesTexture; + uniformMapProperties.vectorPolygonGridCellIndicesTexture = + vectorData.polygonGridCellIndicesTexture; } // update clipping polygons diff --git a/packages/engine/Source/Shaders/GlobeFS.glsl b/packages/engine/Source/Shaders/GlobeFS.glsl index 5d459fbfec4..773c7acde17 100644 --- a/packages/engine/Source/Shaders/GlobeFS.glsl +++ b/packages/engine/Source/Shaders/GlobeFS.glsl @@ -577,6 +577,7 @@ void main() #endif #ifdef HAS_VECTOR_LAYER + finalColor = vectorPolygonRender(v_textureCoordinates.xy, finalColor); finalColor = vectorPolylineRender(v_textureCoordinates.xy, finalColor); #endif diff --git a/packages/engine/Source/Shaders/VectorCommon.glsl b/packages/engine/Source/Shaders/VectorCommon.glsl index 60eb96260f4..a3700f4a307 100644 --- a/packages/engine/Source/Shaders/VectorCommon.glsl +++ b/packages/engine/Source/Shaders/VectorCommon.glsl @@ -3,6 +3,9 @@ uniform highp sampler2D u_vectorWidthTexture; uniform highp sampler2D u_vectorColorTexture; uniform highp sampler2D u_vectorSegmentPrimitiveIndicesTexture; uniform highp sampler2D u_vectorGridCellIndicesTexture; +uniform highp sampler2D u_vectorPolygonEdgeTexture; +uniform highp sampler2D u_vectorPolygonEdgePrimitiveIndicesTexture; +uniform highp sampler2D u_vectorPolygonGridCellIndicesTexture; // UV-space offset from the closest point on the segment to p. vec2 vectorOffsetToLine(vec2 p, vec4 line) @@ -32,6 +35,13 @@ ivec2 vectorIndexToUv(int index, ivec2 size) // vector color is alpha-composited over the terrain (no discard). vec4 vectorPolylineRender(vec2 vectorUv, vec4 baseColor) { + // A tile without polylines binds a 1x1 placeholder; a real grid header + // [gridWidth, gridHeight, ...] is at least 3 texels wide. + if (textureSize(u_vectorGridCellIndicesTexture, 0).x < 3) + { + return baseColor; + } + // Inverse UV-per-pixel Jacobian: measures line distance in screen pixels so // width stays constant under anisotropic (oblique) foreshortening. mat2 screenFromUv = inverse(mat2(dFdx(vectorUv), dFdy(vectorUv))); @@ -71,3 +81,74 @@ vec4 vectorPolylineRender(vec2 vectorUv, vec4 baseColor) return baseColor; } + +// Drape clamped vector polygon fills onto the terrain surface. The fragment's +// tile UV picks a grid cell whose edges were clipped to the cell on the CPU, +// forming closed loops, so an even-odd horizontal ray cast within the cell +// decides coverage. Edges arrive grouped by primitive; each covering +// primitive's fill color is alpha-composited in primitive order (no discard). +vec4 vectorPolygonRender(vec2 vectorUv, vec4 baseColor) +{ + // A tile without polygons binds a 1x1 placeholder; a real grid header + // [gridWidth, gridHeight, ...] is at least 3 texels wide. + if (textureSize(u_vectorPolygonGridCellIndicesTexture, 0).x < 3) + { + return baseColor; + } + + int gridWidth = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(0, 0), 0).r); + int gridHeight = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(1, 0), 0).r); + int cellX = clamp(int(vectorUv.x * float(gridWidth)), 0, gridWidth - 1); + int cellY = clamp(int(vectorUv.y * float(gridHeight)), 0, gridHeight - 1); + int cellIndex = cellX + cellY * gridWidth; + + int indexEnd = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(cellIndex + 2, 0), 0).r); + int indexStart = cellIndex == 0 + ? 0 + : int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(cellIndex + 1, 0), 0).r); + + ivec2 edgeTextureSize = textureSize(u_vectorPolygonEdgeTexture, 0); + ivec2 primitiveTextureSize = textureSize(u_vectorColorTexture, 0); + + int currentPrimitive = -1; + bool inside = false; + + // One extra iteration (i == indexEnd) flushes the final primitive group. + for (int i = indexStart; i <= indexEnd; i++) + { + int primitiveIndex = -1; + vec4 edge = vec4(0.0); + if (i < indexEnd) + { + ivec2 edgeUv = vectorIndexToUv(i, edgeTextureSize); + edge = texelFetch(u_vectorPolygonEdgeTexture, edgeUv, 0); + primitiveIndex = int(texelFetch(u_vectorPolygonEdgePrimitiveIndicesTexture, edgeUv, 0).r); + } + + if (primitiveIndex != currentPrimitive) + { + if (currentPrimitive >= 0 && inside) + { + ivec2 primitiveUv = vectorIndexToUv(currentPrimitive, primitiveTextureSize); + vec4 fillColor = texelFetch(u_vectorColorTexture, primitiveUv, 0); + baseColor = fillColor * vec4(fillColor.aaa, 1.0) + baseColor * (1.0 - fillColor.a); + } + currentPrimitive = primitiveIndex; + inside = false; + } + + // Even-odd rule with a horizontal +x ray; the half-open interval + // (> vs <=) counts a ray through a shared vertex exactly once. + if (i < indexEnd && (edge.y > vectorUv.y) != (edge.w > vectorUv.y)) + { + float t = (vectorUv.y - edge.y) / (edge.w - edge.y); + float xIntersect = edge.x + t * (edge.z - edge.x); + if (vectorUv.x < xIntersect) + { + inside = !inside; + } + } + } + + return baseColor; +} diff --git a/packages/engine/Specs/Core/VectorProviderSpec.js b/packages/engine/Specs/Core/VectorProviderSpec.js index c58b7af7b2e..8449b2a66f8 100644 --- a/packages/engine/Specs/Core/VectorProviderSpec.js +++ b/packages/engine/Specs/Core/VectorProviderSpec.js @@ -1,5 +1,7 @@ import { BoundingSphere, + BufferPolygon, + BufferPolygonCollection, BufferPolyline, BufferPolylineCollection, Cartesian3, @@ -177,4 +179,178 @@ describe("Core/VectorProvider", function () { provider.remove(collection); expect(provider._dirtyRectangles.length).toBe(1); }); + + // A quad around the polyline midpoint (lon -100 to -90, lat 35 to 45), + // with a hole in its middle (lon -97 to -93, lat 38 to 42). + function createPolygonCollection(options) { + const collection = new BufferPolygonCollection({ + primitiveCountMax: 1, + vertexCountMax: 8, + holeCountMax: 1, + triangleCountMax: 8, + heightReference: HeightReference.CLAMP_TO_TERRAIN, + }); + const positions = new Float64Array(24); + Cartesian3.pack(Cartesian3.fromDegrees(-100.0, 35.0), positions, 0); + Cartesian3.pack(Cartesian3.fromDegrees(-90.0, 35.0), positions, 3); + Cartesian3.pack(Cartesian3.fromDegrees(-90.0, 45.0), positions, 6); + Cartesian3.pack(Cartesian3.fromDegrees(-100.0, 45.0), positions, 9); + Cartesian3.pack(Cartesian3.fromDegrees(-97.0, 38.0), positions, 12); + Cartesian3.pack(Cartesian3.fromDegrees(-93.0, 38.0), positions, 15); + Cartesian3.pack(Cartesian3.fromDegrees(-93.0, 42.0), positions, 18); + Cartesian3.pack(Cartesian3.fromDegrees(-97.0, 42.0), positions, 21); + const holes = options?.withHole ? new Uint32Array([4]) : undefined; + const vertexCount = options?.withHole ? 8 : 4; + collection.add( + { + positions: positions.subarray(0, vertexCount * 3), + holes: holes, + }, + new BufferPolygon(), + ); + return collection; + } + + it("returns packed polygon lookup data for a tile overlapping a polygon", function () { + const provider = new VectorProvider({ tilingScheme }); + provider.add(createPolygonCollection()); + + const xy = tilingScheme.positionToTileXY(lineMidpoint, level); + const data = provider.requestTileData(xy.x, xy.y, level, context); + + expect(data.show).toBe(true); + expect(data.polygonEdgeTexels).toBeInstanceOf(Float32Array); + expect(data.polygonGridCellIndices).toBeInstanceOf(Uint32Array); + expect(data.colors.length).toBeGreaterThan(0); + + // Grid header: [gridWidth, gridHeight, ...per-cell end offsets]. + const gridWidth = data.polygonGridCellIndices[0]; + const gridHeight = data.polygonGridCellIndices[1]; + expect(gridWidth).toBeGreaterThan(0); + expect(gridHeight).toBeGreaterThan(0); + expect(data.polygonGridCellIndices.length).toBe(gridWidth * gridHeight + 2); + + // At least one real edge texel was packed (fill value is -1). + let packedCount = 0; + for (let i = 0; i < data.polygonEdgeTexels.length; i++) { + if (data.polygonEdgeTexels[i] >= 0.0) { + packedCount++; + } + } + expect(packedCount).toBeGreaterThan(0); + + // No polyline data was packed. + expect(data.segmentTexture).toBeUndefined(); + + // Every cell's edges must balance to even parity along any horizontal + // line: count crossings for a probe through the cell center. + const cellCount = gridWidth * gridHeight; + for (let cell = 0; cell < cellCount; cell++) { + const start = cell === 0 ? 0 : data.polygonGridCellIndices[cell + 1]; + const end = data.polygonGridCellIndices[cell + 2]; + const cellY = Math.floor(cell / gridWidth); + const probeY = (cellY + 0.5) / gridHeight; + let crossings = 0; + for (let e = start; e < end; e++) { + const ay = data.polygonEdgeTexels[e * 4 + 1]; + const by = data.polygonEdgeTexels[e * 4 + 3]; + if (ay > probeY !== by > probeY) { + crossings++; + } + } + expect(crossings % 2).toBe(0); + } + }); + + // Even-odd ray cast against the packed edges of the cell containing + // (uvX, uvY): returns the number of +x crossings. + function countRayCrossings(data, uvX, uvY) { + const gridWidth = data.polygonGridCellIndices[0]; + const gridHeight = data.polygonGridCellIndices[1]; + const cellX = Math.min(Math.floor(uvX * gridWidth), gridWidth - 1); + const cellY = Math.min(Math.floor(uvY * gridHeight), gridHeight - 1); + const cell = cellX + cellY * gridWidth; + const start = cell === 0 ? 0 : data.polygonGridCellIndices[cell + 1]; + const end = data.polygonGridCellIndices[cell + 2]; + + let crossings = 0; + for (let e = start; e < end; e++) { + const ax = data.polygonEdgeTexels[e * 4]; + const ay = data.polygonEdgeTexels[e * 4 + 1]; + const bx = data.polygonEdgeTexels[e * 4 + 2]; + const by = data.polygonEdgeTexels[e * 4 + 3]; + if (ay > uvY !== by > uvY) { + const t = (uvY - ay) / (by - ay); + if (uvX < ax + t * (bx - ax)) { + crossings++; + } + } + } + return crossings; + } + + it("packs hole rings so interior fragments resolve to even parity", function () { + const provider = new VectorProvider({ tilingScheme }); + provider.add(createPolygonCollection({ withHole: true })); + + const xy = tilingScheme.positionToTileXY(lineMidpoint, level); + const data = provider.requestTileData(xy.x, xy.y, level, context); + expect(data.show).toBe(true); + + const tileRectangle = tilingScheme.tileXYToRectangle(xy.x, xy.y, level); + function toUv(lonDegrees, latDegrees) { + return { + x: + (CesiumMath.toRadians(lonDegrees) - tileRectangle.west) / + (tileRectangle.east - tileRectangle.west), + y: + (CesiumMath.toRadians(latDegrees) - tileRectangle.south) / + (tileRectangle.north - tileRectangle.south), + }; + } + + // (-95, 40) lies inside the hole: even parity, outside the fill. + const holePoint = toUv(-95.0, 40.0); + expect(countRayCrossings(data, holePoint.x, holePoint.y) % 2).toBe(0); + + // (-98.5, 40) lies between the hole and the outer ring: odd parity. + const fillPoint = toUv(-98.5, 40.0); + expect(countRayCrossings(data, fillPoint.x, fillPoint.y) % 2).toBe(1); + }); + + it("returns hidden vector data for a tile not overlapping any polygon", function () { + const provider = new VectorProvider({ tilingScheme }); + provider.add(createPolygonCollection()); + + const xy = tilingScheme.positionToTileXY(farPoint, level); + expect(provider.requestTileData(xy.x, xy.y, level, context)).toEqual({ + show: false, + }); + }); + + it("packs polylines and polygons into a shared primitive index space", function () { + const provider = new VectorProvider({ tilingScheme }); + provider.add(createPolylineCollection()); + provider.add(createPolygonCollection()); + + const xy = tilingScheme.positionToTileXY(lineMidpoint, level); + const data = provider.requestTileData(xy.x, xy.y, level, context); + + expect(data.show).toBe(true); + expect(data.segmentTexels).toBeInstanceOf(Float32Array); + expect(data.polygonEdgeTexels).toBeInstanceOf(Float32Array); + + // One polyline primitive + one polygon primitive share the space. + expect(data.primitiveCount).toBe(2); + + // Polygon edges reference a primitive index beyond the polyline's. + let maxPolygonPrimitive = -1; + for (let i = 0; i < data.polygonEdgePrimitiveIndicesTexels.length; i++) { + maxPolygonPrimitive = Math.max( + maxPolygonPrimitive, + data.polygonEdgePrimitiveIndicesTexels[i], + ); + } + expect(maxPolygonPrimitive).toBe(1); + }); }); From 492be3135b9e32d1568e9f44b189d1f251523a4f Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:12:02 -0400 Subject: [PATCH 2/8] point --- packages/engine/Source/Core/VectorPipeline.js | 124 +++++++++++++++++ packages/engine/Source/Core/VectorProvider.js | 127 +++++++++++++----- .../engine/Specs/Core/VectorProviderSpec.js | 46 +++++++ 3 files changed, 260 insertions(+), 37 deletions(-) diff --git a/packages/engine/Source/Core/VectorPipeline.js b/packages/engine/Source/Core/VectorPipeline.js index 9398fc28d20..c35bbf1d939 100644 --- a/packages/engine/Source/Core/VectorPipeline.js +++ b/packages/engine/Source/Core/VectorPipeline.js @@ -3,6 +3,8 @@ import PixelDatatype from "../Renderer/PixelDatatype.js"; import Sampler from "../Renderer/Sampler.js"; import Texture from "../Renderer/Texture.js"; +import BufferPoint from "../Scene/BufferPoint.js"; +import BufferPointMaterial from "../Scene/BufferPointMaterial.js"; import BufferPolygon from "../Scene/BufferPolygon.js"; import BufferPolygonMaterial from "../Scene/BufferPolygonMaterial.js"; import BufferPolyline from "../Scene/BufferPolyline.js"; @@ -19,6 +21,7 @@ import Rectangle from "./Rectangle.js"; /** @import BufferPrimitive from "../Scene/BufferPrimitive.js"; */ /** @import BufferPrimitiveCollection from "../Scene/BufferPrimitiveCollection.js"; */ +/** @import BufferPointCollection from "../Scene/BufferPointCollection.js"; */ /** @import BufferPolygonCollection from "../Scene/BufferPolygonCollection.js"; */ /** @import BufferPolylineCollection from "../Scene/BufferPolylineCollection.js"; */ /** @import Context from "../Renderer/Context.js"; */ @@ -36,6 +39,8 @@ const scratchPolyline = new BufferPolyline(); const scratchPolylineMaterial = new BufferPolylineMaterial(); const scratchPolygon = new BufferPolygon(); const scratchPolygonMaterial = new BufferPolygonMaterial(); +const scratchPoint = new BufferPoint(); +const scratchPointMaterial = new BufferPointMaterial(); const scratchLocalPosition = new Cartesian3(); const scratchWorldPosition = new Cartesian3(); const scratchCartographic = new Cartographic(); @@ -334,6 +339,125 @@ class VectorPipeline { result.gridCellIndices = gridCellIndices; } + /** + * @param {BufferPointCollection} collection + * @param {TilingScheme} tilingScheme + * @param {VectorCollectionData} [result] + * @returns {VectorCollectionData} + */ + static packPointCollectionData(collection, tilingScheme, result) { + if ( + defined(result) && + collection._dirtyCount === 0 && + collection._version === result.version + ) { + return result; + } + + const primitiveCount = collection.primitiveCount; + const boundingVolume = collection.boundingVolume; + const ellipsoid = tilingScheme.ellipsoid; + + const rectangle = Rectangle.fromBoundingSphere(boundingVolume, ellipsoid); + const positions = _getProjectedPositions(collection, ellipsoid); + + const widths = new Uint8Array(primitiveCount); + const colors = new Uint8Array(primitiveCount * 4); + + for (let i = 0; i < primitiveCount; i++) { + const point = /** @type {BufferPoint} */ ( + collection.get(i, scratchPoint) + ); + + // Append materials unconditionally, to simplify indexing and updates. + const pointMaterial = /** @type {BufferPointMaterial} */ ( + point.getMaterial(scratchPointMaterial) + ); + + // The shader tests distance from center, so store the radius. + widths[i] = Math.round(pointMaterial.size * 0.5); + + colors[i * 4] = Color.floatToByte(pointMaterial.color.red); + colors[i * 4 + 1] = Color.floatToByte(pointMaterial.color.green); + colors[i * 4 + 2] = Color.floatToByte(pointMaterial.color.blue); + colors[i * 4 + 3] = Color.floatToByte(pointMaterial.color.alpha); + } + + return Object.assign( + result ?? {}, + /** @type {VectorCollectionData} */ ({ + version: collection._version, + rectangle: rectangle, + positions: positions, + widths: widths, + colors: colors, + }), + ); + } + + /** + * Projects all visible points in a collection into tile-local UV space, + * appending each point inside the tile as a zero-length segment. The + * shader's segment distance test then renders a screen-space circle + * around it, so points share the polyline lookup textures. + * + * @param {BufferPointCollection} collection + * @param {VectorCollectionData} collectionData + * @param {Rectangle} rectangle + * @param {number} width + * @param {VectorTileData} result + */ + static packPointSegments( + collection, + collectionData, + rectangle, + width, + result, + ) { + result.segments ??= []; + result.widths ??= []; + result.colors ??= []; + result.segmentPrimitiveIndices ??= []; + result.primitiveCount ??= 0; + + const primitiveCount = collection.primitiveCount; + const positions = collectionData.positions; + const height = rectangle.north - rectangle.south; + const center = rectangle.west + width * 0.5; + const margin = CesiumMath.EPSILON3; + + for (let i = 0; i < primitiveCount; i++) { + const point = /** @type {BufferPoint} */ ( + collection.get(i, scratchPoint) + ); + if (!point.show) { + continue; + } + + const vertexOffset = point.vertexOffset; + // Shift to the 2π frame nearest the tile center (antimeridian). + const lon = + center + + CesiumMath.negativePiToPi(positions[vertexOffset * 2] - center); + const lat = positions[vertexOffset * 2 + 1]; + + const u = (lon - rectangle.west) / width; + const v = (lat - rectangle.south) / height; + if (u < -margin || u > 1.0 + margin || v < -margin || v > 1.0 + margin) { + continue; + } + + result.segments.push([u, v, u, v]); + result.segmentPrimitiveIndices.push(result.primitiveCount + i); + } + + // Append materials unconditionally, to simplify indexing and updates. + result.widths.push(collectionData.widths); + result.colors.push(collectionData.colors); + + result.primitiveCount += primitiveCount; + } + /** * @param {BufferPolygonCollection} collection * @param {TilingScheme} tilingScheme diff --git a/packages/engine/Source/Core/VectorProvider.js b/packages/engine/Source/Core/VectorProvider.js index abe6d4fc8f0..45bcd20d94d 100644 --- a/packages/engine/Source/Core/VectorProvider.js +++ b/packages/engine/Source/Core/VectorProvider.js @@ -1,7 +1,9 @@ // @ts-check +import BufferPointCollection from "../Scene/BufferPointCollection.js"; import BufferPolygonCollection from "../Scene/BufferPolygonCollection.js"; import BufferPolylineCollection from "../Scene/BufferPolylineCollection.js"; +import CesiumMath from "./Math.js"; import Rectangle from "./Rectangle.js"; import defined from "./defined.js"; import VectorPipeline from "./VectorPipeline.js"; @@ -18,6 +20,34 @@ const scratchTileRectangle = new Rectangle(); const scratchCollectionRectangle = new Rectangle(); const scratchIntersectRectangle = new Rectangle(); +/** + * Packing functions for a collection type. + * + * @typedef {object} CollectionPacker + * @property {(collection: *, tilingScheme: TilingScheme, result?: VectorCollectionData) => VectorCollectionData} packCollectionData Extracts the per-collection snapshot. + * @property {(collection: *, collectionData: VectorCollectionData, rectangle: Rectangle, width: number, result: VectorTileData) => void} packTilePrimitives Packs the collection's primitives into a tile. + * @private + */ + +/** + * Per-type packing functions, keyed by collection class. + * @type {Map} + * @private + */ +const collectionPackers = new Map(); +collectionPackers.set(BufferPolylineCollection, { + packCollectionData: VectorPipeline.packPolylineCollectionData, + packTilePrimitives: VectorPipeline.packPolylineSegments, +}); +collectionPackers.set(BufferPolygonCollection, { + packCollectionData: VectorPipeline.packPolygonCollectionData, + packTilePrimitives: VectorPipeline.packPolygonRings, +}); +collectionPackers.set(BufferPointCollection, { + packCollectionData: VectorPipeline.packPointCollectionData, + packTilePrimitives: VectorPipeline.packPointSegments, +}); + /** * @typedef {object} VectorProviderConstructorOptions * @property {TilingScheme} tilingScheme @@ -155,12 +185,13 @@ class VectorProvider { * @private */ _markCollectionRegionDirty(collection) { - const collectionRectangle = Rectangle.fromBoundingSphere( - collection.boundingVolume, - this._tilingScheme.ellipsoid, - new Rectangle(), + this._dirtyRectangles.push( + computeCollectionRectangle( + collection, + this._tilingScheme.ellipsoid, + new Rectangle(), + ), ); - this._dirtyRectangles.push(collectionRectangle); } /** @@ -179,8 +210,13 @@ class VectorProvider { const result = { show: true }; for (const collection of this._collections) { - const collectionRectangle = Rectangle.fromBoundingSphere( - collection.boundingVolume, + const packer = collectionPackers.get(collection.constructor); + if (!defined(packer)) { + continue; + } + + const collectionRectangle = computeCollectionRectangle( + collection, tilingScheme.ellipsoid, scratchCollectionRectangle, ); @@ -195,43 +231,30 @@ class VectorProvider { continue; } - if (collection instanceof BufferPolylineCollection) { - const collectionData = this._getCollectionDataCached( - collection, - VectorPipeline.packPolylineCollectionData, - ); - VectorPipeline.packPolylineSegments( - collection, - collectionData, - tileRectangle, - width, - result, - ); - } else if (collection instanceof BufferPolygonCollection) { - const collectionData = this._getCollectionDataCached( - collection, - VectorPipeline.packPolygonCollectionData, - ); - VectorPipeline.packPolygonRings( - collection, - collectionData, - tileRectangle, - width, - result, - ); - } + const collectionData = this._getCollectionDataCached( + collection, + packer.packCollectionData, + ); + packer.packTilePrimitives( + collection, + collectionData, + tileRectangle, + width, + result, + ); } - const hasPolylines = defined(result.segments) && result.segments.length > 0; + // Points pack as zero-length segments, sharing the polyline lookup. + const hasSegments = defined(result.segments) && result.segments.length > 0; const hasPolygons = defined(result.polygonRings) && result.polygonRings.length > 0; - if (!hasPolylines && !hasPolygons) { + if (!hasSegments && !hasPolygons) { result.show = false; return result; } - if (hasPolylines) { + if (hasSegments) { VectorPipeline.packPolylineGrid(result); VectorPipeline.packPolylineTextures(context, result); } @@ -337,8 +360,8 @@ class VectorProvider { // If dirty, the version increments +1 when marked clean below. data.version = collection._version + (dirty ? 1 : 0); - data.rectangle = Rectangle.fromBoundingSphere( - collection.boundingVolume, + data.rectangle = computeCollectionRectangle( + collection, this.ellipsoid, data.rectangle, ); @@ -350,6 +373,36 @@ class VectorProvider { } } +/** + * Computes the cartographic rectangle covered by a collection. A collection + * of a single point has a zero-radius bounding sphere, which projects to a + * zero-area rectangle that {@link Rectangle.intersection} treats as empty; + * expand degenerate extents by a small epsilon so such collections still + * intersect the tiles containing them. + * + * @param {BufferPrimitiveCollection} collection + * @param {Ellipsoid} ellipsoid + * @param {Rectangle} [result] + * @returns {Rectangle} + * @private + */ +function computeCollectionRectangle(collection, ellipsoid, result) { + result = Rectangle.fromBoundingSphere( + collection.boundingVolume, + ellipsoid, + result, + ); + if (result.east - result.west < CesiumMath.EPSILON10) { + result.west -= CesiumMath.EPSILON10; + result.east += CesiumMath.EPSILON10; + } + if (result.north - result.south < CesiumMath.EPSILON10) { + result.south -= CesiumMath.EPSILON10; + result.north += CesiumMath.EPSILON10; + } + return result; +} + /** * @param {number} x * @param {number} y diff --git a/packages/engine/Specs/Core/VectorProviderSpec.js b/packages/engine/Specs/Core/VectorProviderSpec.js index 8449b2a66f8..5fcd7d2885f 100644 --- a/packages/engine/Specs/Core/VectorProviderSpec.js +++ b/packages/engine/Specs/Core/VectorProviderSpec.js @@ -1,5 +1,8 @@ import { BoundingSphere, + BufferPoint, + BufferPointCollection, + BufferPointMaterial, BufferPolygon, BufferPolygonCollection, BufferPolyline, @@ -353,4 +356,47 @@ describe("Core/VectorProvider", function () { } expect(maxPolygonPrimitive).toBe(1); }); + + function createPointCollection() { + const collection = new BufferPointCollection({ + primitiveCountMax: 1, + vertexCountMax: 1, + heightReference: HeightReference.CLAMP_TO_TERRAIN, + }); + const point = collection.add( + { position: Cartesian3.fromDegrees(-95.0, 40.0) }, + new BufferPoint(), + ); + point.setMaterial(new BufferPointMaterial({ size: 10 })); + return collection; + } + + it("packs points as zero-length segments with half-size widths", function () { + const provider = new VectorProvider({ tilingScheme }); + provider.add(createPointCollection()); + + const xy = tilingScheme.positionToTileXY(lineMidpoint, level); + const data = provider.requestTileData(xy.x, xy.y, level, context); + + expect(data.show).toBe(true); + expect(data.segments.length).toBe(1); + + // Zero-length segment at the point's tile UV. + const segment = data.segments[0]; + expect(segment[0]).toBe(segment[2]); + expect(segment[1]).toBe(segment[3]); + + // The shader tests distance from center, so size 10 stores radius 5. + expect(data.widths[0][0]).toBe(5); + }); + + it("returns hidden vector data for a tile not overlapping any point", function () { + const provider = new VectorProvider({ tilingScheme }); + provider.add(createPointCollection()); + + const xy = tilingScheme.positionToTileXY(farPoint, level); + expect(provider.requestTileData(xy.x, xy.y, level, context)).toEqual({ + show: false, + }); + }); }); From 44345faa054898e5095ce99d690254b08f391660 Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:42:00 -0400 Subject: [PATCH 3/8] fix --- packages/engine/Source/Core/VectorProvider.js | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/engine/Source/Core/VectorProvider.js b/packages/engine/Source/Core/VectorProvider.js index 45bcd20d94d..b79f53a74dc 100644 --- a/packages/engine/Source/Core/VectorProvider.js +++ b/packages/engine/Source/Core/VectorProvider.js @@ -20,12 +20,37 @@ const scratchTileRectangle = new Rectangle(); const scratchCollectionRectangle = new Rectangle(); const scratchIntersectRectangle = new Rectangle(); +/** + * Extracts a collection's snapshot of projected positions and per-primitive + * material properties. + * + * @callback PackCollectionData + * @param {*} collection + * @param {TilingScheme} tilingScheme + * @param {VectorCollectionData} [result] + * @returns {VectorCollectionData} + * @private + */ + +/** + * Packs a collection's primitives into a tile's vector data. + * + * @callback PackTilePrimitives + * @param {*} collection + * @param {VectorCollectionData} collectionData + * @param {Rectangle} rectangle + * @param {number} width + * @param {VectorTileData} result + * @returns {void} + * @private + */ + /** * Packing functions for a collection type. * * @typedef {object} CollectionPacker - * @property {(collection: *, tilingScheme: TilingScheme, result?: VectorCollectionData) => VectorCollectionData} packCollectionData Extracts the per-collection snapshot. - * @property {(collection: *, collectionData: VectorCollectionData, rectangle: Rectangle, width: number, result: VectorTileData) => void} packTilePrimitives Packs the collection's primitives into a tile. + * @property {PackCollectionData} packCollectionData Extracts the per-collection snapshot. + * @property {PackTilePrimitives} packTilePrimitives Packs the collection's primitives into a tile. * @private */ @@ -343,7 +368,7 @@ class VectorProvider { * clean only after everything has been read back. * * @param {BufferPrimitiveCollection} collection - * @param {(collection: *, tilingScheme: TilingScheme, result?: VectorCollectionData) => VectorCollectionData} packCollectionData + * @param {PackCollectionData} packCollectionData * @returns {VectorCollectionData} * @private */ From 68260f1dd8f90bdb3d827bcc17aac2faa7c0b6ca Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:05:41 -0400 Subject: [PATCH 4/8] fix --- packages/engine/Source/Core/VectorPipeline.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/engine/Source/Core/VectorPipeline.js b/packages/engine/Source/Core/VectorPipeline.js index c35bbf1d939..db02c083ba7 100644 --- a/packages/engine/Source/Core/VectorPipeline.js +++ b/packages/engine/Source/Core/VectorPipeline.js @@ -619,7 +619,8 @@ class VectorPipeline { Math.ceil(Math.sqrt(ringEdgeCount / GRID_TARGET_SEGMENTS_PER_CELL)), ); - /** @type {number[][]} Per-cell packed edges [ax, ay, bx, by, primitiveIndex, ...]. */ + // Per-cell packed edges [ax, ay, bx, by, primitiveIndex, ...]. + /** @type {number[][]} */ const grid = new Array(gridSize * gridSize); for (let i = 0; i < grid.length; i++) { grid[i] = []; From b2baaf5beabe445436a6d09a466b36b123503369 Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:15:48 -0400 Subject: [PATCH 5/8] add CHANGES.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 86aaf51407a..fc18d652606 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ - Added `Texture.defaultColor` static property to allow customizing the default placeholder texture color. [#13597](https://github.com/CesiumGS/cesium/pull/13597) - Added support for draping clamped vector tile polylines onto terrain, with screen-space-constant line width and per-feature styling via `Cesium3DTileStyle`. [#13577](https://github.com/CesiumGS/cesium/pull/13577) +- Added support for draping clamped vector tile polygons and points onto terrain, completing terrain draping for all vector primitive types. [#13627](https://github.com/CesiumGS/cesium/pull/13627) #### Fixes :wrench: From dd3ed820ce99193787f308548dab1b94067a86da Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:51:05 -0400 Subject: [PATCH 6/8] remove points --- CHANGES.md | 2 +- packages/engine/Source/Core/VectorPipeline.js | 124 ------------------ packages/engine/Source/Core/VectorProvider.js | 62 ++------- .../engine/Specs/Core/VectorProviderSpec.js | 46 ------- 4 files changed, 13 insertions(+), 221 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fc18d652606..43803b6c9f1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ - Added `Texture.defaultColor` static property to allow customizing the default placeholder texture color. [#13597](https://github.com/CesiumGS/cesium/pull/13597) - Added support for draping clamped vector tile polylines onto terrain, with screen-space-constant line width and per-feature styling via `Cesium3DTileStyle`. [#13577](https://github.com/CesiumGS/cesium/pull/13577) -- Added support for draping clamped vector tile polygons and points onto terrain, completing terrain draping for all vector primitive types. [#13627](https://github.com/CesiumGS/cesium/pull/13627) +- Added support for draping clamped vector tile polygons onto terrain, complementing the polyline support added in [#13577](https://github.com/CesiumGS/cesium/pull/13577). [#13627](https://github.com/CesiumGS/cesium/pull/13627) #### Fixes :wrench: diff --git a/packages/engine/Source/Core/VectorPipeline.js b/packages/engine/Source/Core/VectorPipeline.js index db02c083ba7..7abca5f42a7 100644 --- a/packages/engine/Source/Core/VectorPipeline.js +++ b/packages/engine/Source/Core/VectorPipeline.js @@ -3,8 +3,6 @@ import PixelDatatype from "../Renderer/PixelDatatype.js"; import Sampler from "../Renderer/Sampler.js"; import Texture from "../Renderer/Texture.js"; -import BufferPoint from "../Scene/BufferPoint.js"; -import BufferPointMaterial from "../Scene/BufferPointMaterial.js"; import BufferPolygon from "../Scene/BufferPolygon.js"; import BufferPolygonMaterial from "../Scene/BufferPolygonMaterial.js"; import BufferPolyline from "../Scene/BufferPolyline.js"; @@ -21,7 +19,6 @@ import Rectangle from "./Rectangle.js"; /** @import BufferPrimitive from "../Scene/BufferPrimitive.js"; */ /** @import BufferPrimitiveCollection from "../Scene/BufferPrimitiveCollection.js"; */ -/** @import BufferPointCollection from "../Scene/BufferPointCollection.js"; */ /** @import BufferPolygonCollection from "../Scene/BufferPolygonCollection.js"; */ /** @import BufferPolylineCollection from "../Scene/BufferPolylineCollection.js"; */ /** @import Context from "../Renderer/Context.js"; */ @@ -39,8 +36,6 @@ const scratchPolyline = new BufferPolyline(); const scratchPolylineMaterial = new BufferPolylineMaterial(); const scratchPolygon = new BufferPolygon(); const scratchPolygonMaterial = new BufferPolygonMaterial(); -const scratchPoint = new BufferPoint(); -const scratchPointMaterial = new BufferPointMaterial(); const scratchLocalPosition = new Cartesian3(); const scratchWorldPosition = new Cartesian3(); const scratchCartographic = new Cartographic(); @@ -339,125 +334,6 @@ class VectorPipeline { result.gridCellIndices = gridCellIndices; } - /** - * @param {BufferPointCollection} collection - * @param {TilingScheme} tilingScheme - * @param {VectorCollectionData} [result] - * @returns {VectorCollectionData} - */ - static packPointCollectionData(collection, tilingScheme, result) { - if ( - defined(result) && - collection._dirtyCount === 0 && - collection._version === result.version - ) { - return result; - } - - const primitiveCount = collection.primitiveCount; - const boundingVolume = collection.boundingVolume; - const ellipsoid = tilingScheme.ellipsoid; - - const rectangle = Rectangle.fromBoundingSphere(boundingVolume, ellipsoid); - const positions = _getProjectedPositions(collection, ellipsoid); - - const widths = new Uint8Array(primitiveCount); - const colors = new Uint8Array(primitiveCount * 4); - - for (let i = 0; i < primitiveCount; i++) { - const point = /** @type {BufferPoint} */ ( - collection.get(i, scratchPoint) - ); - - // Append materials unconditionally, to simplify indexing and updates. - const pointMaterial = /** @type {BufferPointMaterial} */ ( - point.getMaterial(scratchPointMaterial) - ); - - // The shader tests distance from center, so store the radius. - widths[i] = Math.round(pointMaterial.size * 0.5); - - colors[i * 4] = Color.floatToByte(pointMaterial.color.red); - colors[i * 4 + 1] = Color.floatToByte(pointMaterial.color.green); - colors[i * 4 + 2] = Color.floatToByte(pointMaterial.color.blue); - colors[i * 4 + 3] = Color.floatToByte(pointMaterial.color.alpha); - } - - return Object.assign( - result ?? {}, - /** @type {VectorCollectionData} */ ({ - version: collection._version, - rectangle: rectangle, - positions: positions, - widths: widths, - colors: colors, - }), - ); - } - - /** - * Projects all visible points in a collection into tile-local UV space, - * appending each point inside the tile as a zero-length segment. The - * shader's segment distance test then renders a screen-space circle - * around it, so points share the polyline lookup textures. - * - * @param {BufferPointCollection} collection - * @param {VectorCollectionData} collectionData - * @param {Rectangle} rectangle - * @param {number} width - * @param {VectorTileData} result - */ - static packPointSegments( - collection, - collectionData, - rectangle, - width, - result, - ) { - result.segments ??= []; - result.widths ??= []; - result.colors ??= []; - result.segmentPrimitiveIndices ??= []; - result.primitiveCount ??= 0; - - const primitiveCount = collection.primitiveCount; - const positions = collectionData.positions; - const height = rectangle.north - rectangle.south; - const center = rectangle.west + width * 0.5; - const margin = CesiumMath.EPSILON3; - - for (let i = 0; i < primitiveCount; i++) { - const point = /** @type {BufferPoint} */ ( - collection.get(i, scratchPoint) - ); - if (!point.show) { - continue; - } - - const vertexOffset = point.vertexOffset; - // Shift to the 2π frame nearest the tile center (antimeridian). - const lon = - center + - CesiumMath.negativePiToPi(positions[vertexOffset * 2] - center); - const lat = positions[vertexOffset * 2 + 1]; - - const u = (lon - rectangle.west) / width; - const v = (lat - rectangle.south) / height; - if (u < -margin || u > 1.0 + margin || v < -margin || v > 1.0 + margin) { - continue; - } - - result.segments.push([u, v, u, v]); - result.segmentPrimitiveIndices.push(result.primitiveCount + i); - } - - // Append materials unconditionally, to simplify indexing and updates. - result.widths.push(collectionData.widths); - result.colors.push(collectionData.colors); - - result.primitiveCount += primitiveCount; - } - /** * @param {BufferPolygonCollection} collection * @param {TilingScheme} tilingScheme diff --git a/packages/engine/Source/Core/VectorProvider.js b/packages/engine/Source/Core/VectorProvider.js index b79f53a74dc..435ad4070da 100644 --- a/packages/engine/Source/Core/VectorProvider.js +++ b/packages/engine/Source/Core/VectorProvider.js @@ -1,9 +1,7 @@ // @ts-check -import BufferPointCollection from "../Scene/BufferPointCollection.js"; import BufferPolygonCollection from "../Scene/BufferPolygonCollection.js"; import BufferPolylineCollection from "../Scene/BufferPolylineCollection.js"; -import CesiumMath from "./Math.js"; import Rectangle from "./Rectangle.js"; import defined from "./defined.js"; import VectorPipeline from "./VectorPipeline.js"; @@ -68,10 +66,6 @@ collectionPackers.set(BufferPolygonCollection, { packCollectionData: VectorPipeline.packPolygonCollectionData, packTilePrimitives: VectorPipeline.packPolygonRings, }); -collectionPackers.set(BufferPointCollection, { - packCollectionData: VectorPipeline.packPointCollectionData, - packTilePrimitives: VectorPipeline.packPointSegments, -}); /** * @typedef {object} VectorProviderConstructorOptions @@ -210,13 +204,12 @@ class VectorProvider { * @private */ _markCollectionRegionDirty(collection) { - this._dirtyRectangles.push( - computeCollectionRectangle( - collection, - this._tilingScheme.ellipsoid, - new Rectangle(), - ), + const collectionRectangle = Rectangle.fromBoundingSphere( + collection.boundingVolume, + this._tilingScheme.ellipsoid, + new Rectangle(), ); + this._dirtyRectangles.push(collectionRectangle); } /** @@ -240,8 +233,8 @@ class VectorProvider { continue; } - const collectionRectangle = computeCollectionRectangle( - collection, + const collectionRectangle = Rectangle.fromBoundingSphere( + collection.boundingVolume, tilingScheme.ellipsoid, scratchCollectionRectangle, ); @@ -269,17 +262,16 @@ class VectorProvider { ); } - // Points pack as zero-length segments, sharing the polyline lookup. - const hasSegments = defined(result.segments) && result.segments.length > 0; + const hasPolylines = defined(result.segments) && result.segments.length > 0; const hasPolygons = defined(result.polygonRings) && result.polygonRings.length > 0; - if (!hasSegments && !hasPolygons) { + if (!hasPolylines && !hasPolygons) { result.show = false; return result; } - if (hasSegments) { + if (hasPolylines) { VectorPipeline.packPolylineGrid(result); VectorPipeline.packPolylineTextures(context, result); } @@ -385,8 +377,8 @@ class VectorProvider { // If dirty, the version increments +1 when marked clean below. data.version = collection._version + (dirty ? 1 : 0); - data.rectangle = computeCollectionRectangle( - collection, + data.rectangle = Rectangle.fromBoundingSphere( + collection.boundingVolume, this.ellipsoid, data.rectangle, ); @@ -398,36 +390,6 @@ class VectorProvider { } } -/** - * Computes the cartographic rectangle covered by a collection. A collection - * of a single point has a zero-radius bounding sphere, which projects to a - * zero-area rectangle that {@link Rectangle.intersection} treats as empty; - * expand degenerate extents by a small epsilon so such collections still - * intersect the tiles containing them. - * - * @param {BufferPrimitiveCollection} collection - * @param {Ellipsoid} ellipsoid - * @param {Rectangle} [result] - * @returns {Rectangle} - * @private - */ -function computeCollectionRectangle(collection, ellipsoid, result) { - result = Rectangle.fromBoundingSphere( - collection.boundingVolume, - ellipsoid, - result, - ); - if (result.east - result.west < CesiumMath.EPSILON10) { - result.west -= CesiumMath.EPSILON10; - result.east += CesiumMath.EPSILON10; - } - if (result.north - result.south < CesiumMath.EPSILON10) { - result.south -= CesiumMath.EPSILON10; - result.north += CesiumMath.EPSILON10; - } - return result; -} - /** * @param {number} x * @param {number} y diff --git a/packages/engine/Specs/Core/VectorProviderSpec.js b/packages/engine/Specs/Core/VectorProviderSpec.js index 5fcd7d2885f..8449b2a66f8 100644 --- a/packages/engine/Specs/Core/VectorProviderSpec.js +++ b/packages/engine/Specs/Core/VectorProviderSpec.js @@ -1,8 +1,5 @@ import { BoundingSphere, - BufferPoint, - BufferPointCollection, - BufferPointMaterial, BufferPolygon, BufferPolygonCollection, BufferPolyline, @@ -356,47 +353,4 @@ describe("Core/VectorProvider", function () { } expect(maxPolygonPrimitive).toBe(1); }); - - function createPointCollection() { - const collection = new BufferPointCollection({ - primitiveCountMax: 1, - vertexCountMax: 1, - heightReference: HeightReference.CLAMP_TO_TERRAIN, - }); - const point = collection.add( - { position: Cartesian3.fromDegrees(-95.0, 40.0) }, - new BufferPoint(), - ); - point.setMaterial(new BufferPointMaterial({ size: 10 })); - return collection; - } - - it("packs points as zero-length segments with half-size widths", function () { - const provider = new VectorProvider({ tilingScheme }); - provider.add(createPointCollection()); - - const xy = tilingScheme.positionToTileXY(lineMidpoint, level); - const data = provider.requestTileData(xy.x, xy.y, level, context); - - expect(data.show).toBe(true); - expect(data.segments.length).toBe(1); - - // Zero-length segment at the point's tile UV. - const segment = data.segments[0]; - expect(segment[0]).toBe(segment[2]); - expect(segment[1]).toBe(segment[3]); - - // The shader tests distance from center, so size 10 stores radius 5. - expect(data.widths[0][0]).toBe(5); - }); - - it("returns hidden vector data for a tile not overlapping any point", function () { - const provider = new VectorProvider({ tilingScheme }); - provider.add(createPointCollection()); - - const xy = tilingScheme.positionToTileXY(farPoint, level); - expect(provider.requestTileData(xy.x, xy.y, level, context)).toEqual({ - show: false, - }); - }); }); From 2ce10a6e58ceda1fa37aae51a6c6d61fa822e98a Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:40:45 -0400 Subject: [PATCH 7/8] Texture cap --- packages/engine/Source/Core/VectorPipeline.js | 59 +++++++++++-------- .../engine/Source/Shaders/VectorCommon.glsl | 26 ++++---- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/packages/engine/Source/Core/VectorPipeline.js b/packages/engine/Source/Core/VectorPipeline.js index 7abca5f42a7..1261462d368 100644 --- a/packages/engine/Source/Core/VectorPipeline.js +++ b/packages/engine/Source/Core/VectorPipeline.js @@ -672,18 +672,10 @@ class VectorPipeline { flipY: false, }); - result.gridCellIndicesTexture = new Texture({ - context: context, - pixelFormat: PixelFormat.RED, - pixelDatatype: PixelDatatype.FLOAT, - source: { - width: result.gridCellIndices.length, - height: 1, - arrayBufferView: new Float32Array(result.gridCellIndices), - }, - sampler: Sampler.NEAREST, - flipY: false, - }); + result.gridCellIndicesTexture = _createGridCellIndicesTexture( + context, + result.gridCellIndices, + ); } /** @@ -717,18 +709,10 @@ class VectorPipeline { flipY: false, }); - result.polygonGridCellIndicesTexture = new Texture({ - context: context, - pixelFormat: PixelFormat.RED, - pixelDatatype: PixelDatatype.FLOAT, - source: { - width: result.polygonGridCellIndices.length, - height: 1, - arrayBufferView: new Float32Array(result.polygonGridCellIndices), - }, - sampler: Sampler.NEAREST, - flipY: false, - }); + result.polygonGridCellIndicesTexture = _createGridCellIndicesTexture( + context, + result.polygonGridCellIndices, + ); } /** @@ -1010,6 +994,33 @@ function _nextPowerOfTwoSize(count) { return [width, height]; } +/** + * Creates a grid header texture, wrapped to a 2D power-of-two size so the + * header length is not limited by the maximum texture width. + * + * @param {Context} context + * @param {Uint32Array} gridCellIndices + * @returns {Texture} + * @private + */ +function _createGridCellIndicesTexture(context, gridCellIndices) { + const [width, height] = _nextPowerOfTwoSize(gridCellIndices.length); + const texels = new Float32Array(width * height); + texels.set(gridCellIndices); + return new Texture({ + context: context, + pixelFormat: PixelFormat.RED, + pixelDatatype: PixelDatatype.FLOAT, + source: { + width: width, + height: height, + arrayBufferView: texels, + }, + sampler: Sampler.NEAREST, + flipY: false, + }); +} + /** * Returns the collection's vertex positions, projected to [lng, lat] * coordinates, in radians, on the given ellipsoid. diff --git a/packages/engine/Source/Shaders/VectorCommon.glsl b/packages/engine/Source/Shaders/VectorCommon.glsl index a3700f4a307..9af4f254ac6 100644 --- a/packages/engine/Source/Shaders/VectorCommon.glsl +++ b/packages/engine/Source/Shaders/VectorCommon.glsl @@ -36,8 +36,9 @@ ivec2 vectorIndexToUv(int index, ivec2 size) vec4 vectorPolylineRender(vec2 vectorUv, vec4 baseColor) { // A tile without polylines binds a 1x1 placeholder; a real grid header - // [gridWidth, gridHeight, ...] is at least 3 texels wide. - if (textureSize(u_vectorGridCellIndicesTexture, 0).x < 3) + // [gridWidth, gridHeight, ...] is at least 3 texels. + ivec2 headerSize = textureSize(u_vectorGridCellIndicesTexture, 0); + if (headerSize.x * headerSize.y < 3) { return baseColor; } @@ -45,16 +46,16 @@ vec4 vectorPolylineRender(vec2 vectorUv, vec4 baseColor) // Inverse UV-per-pixel Jacobian: measures line distance in screen pixels so // width stays constant under anisotropic (oblique) foreshortening. mat2 screenFromUv = inverse(mat2(dFdx(vectorUv), dFdy(vectorUv))); - int gridWidth = int(texelFetch(u_vectorGridCellIndicesTexture, ivec2(0, 0), 0).r); - int gridHeight = int(texelFetch(u_vectorGridCellIndicesTexture, ivec2(1, 0), 0).r); + int gridWidth = int(texelFetch(u_vectorGridCellIndicesTexture, vectorIndexToUv(0, headerSize), 0).r); + int gridHeight = int(texelFetch(u_vectorGridCellIndicesTexture, vectorIndexToUv(1, headerSize), 0).r); int cellX = clamp(int(vectorUv.x * float(gridWidth)), 0, gridWidth - 1); int cellY = clamp(int(vectorUv.y * float(gridHeight)), 0, gridHeight - 1); int cellIndex = cellX + cellY * gridWidth; - int indexEnd = int(texelFetch(u_vectorGridCellIndicesTexture, ivec2(cellIndex + 2, 0), 0).r); + int indexEnd = int(texelFetch(u_vectorGridCellIndicesTexture, vectorIndexToUv(cellIndex + 2, headerSize), 0).r); int indexStart = cellIndex == 0 ? 0 - : int(texelFetch(u_vectorGridCellIndicesTexture, ivec2(cellIndex + 1, 0), 0).r); + : int(texelFetch(u_vectorGridCellIndicesTexture, vectorIndexToUv(cellIndex + 1, headerSize), 0).r); ivec2 segmentTextureSize = textureSize(u_vectorSegmentTexture, 0); ivec2 primitiveTextureSize = textureSize(u_vectorWidthTexture, 0); @@ -90,22 +91,23 @@ vec4 vectorPolylineRender(vec2 vectorUv, vec4 baseColor) vec4 vectorPolygonRender(vec2 vectorUv, vec4 baseColor) { // A tile without polygons binds a 1x1 placeholder; a real grid header - // [gridWidth, gridHeight, ...] is at least 3 texels wide. - if (textureSize(u_vectorPolygonGridCellIndicesTexture, 0).x < 3) + // [gridWidth, gridHeight, ...] is at least 3 texels. + ivec2 headerSize = textureSize(u_vectorPolygonGridCellIndicesTexture, 0); + if (headerSize.x * headerSize.y < 3) { return baseColor; } - int gridWidth = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(0, 0), 0).r); - int gridHeight = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(1, 0), 0).r); + int gridWidth = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, vectorIndexToUv(0, headerSize), 0).r); + int gridHeight = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, vectorIndexToUv(1, headerSize), 0).r); int cellX = clamp(int(vectorUv.x * float(gridWidth)), 0, gridWidth - 1); int cellY = clamp(int(vectorUv.y * float(gridHeight)), 0, gridHeight - 1); int cellIndex = cellX + cellY * gridWidth; - int indexEnd = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(cellIndex + 2, 0), 0).r); + int indexEnd = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, vectorIndexToUv(cellIndex + 2, headerSize), 0).r); int indexStart = cellIndex == 0 ? 0 - : int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(cellIndex + 1, 0), 0).r); + : int(texelFetch(u_vectorPolygonGridCellIndicesTexture, vectorIndexToUv(cellIndex + 1, headerSize), 0).r); ivec2 edgeTextureSize = textureSize(u_vectorPolygonEdgeTexture, 0); ivec2 primitiveTextureSize = textureSize(u_vectorColorTexture, 0); From 7c6891ec820c230901ca0c2327874e3c2a124dac Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:43:27 -0400 Subject: [PATCH 8/8] fix --- packages/engine/Source/Core/VectorPipeline.js | 20 ++---- packages/engine/Source/Core/VectorProvider.js | 3 - .../engine/Source/Shaders/VectorCommon.glsl | 72 ++++++++++++------- 3 files changed, 51 insertions(+), 44 deletions(-) diff --git a/packages/engine/Source/Core/VectorPipeline.js b/packages/engine/Source/Core/VectorPipeline.js index 1261462d368..d3c0083488a 100644 --- a/packages/engine/Source/Core/VectorPipeline.js +++ b/packages/engine/Source/Core/VectorPipeline.js @@ -171,22 +171,16 @@ class VectorPipeline { * @param {BufferPolylineCollection} collection * @param {VectorCollectionData} collectionData * @param {Rectangle} rectangle - * @param {number} width * @param {VectorTileData} result */ - static packPolylineSegments( - collection, - collectionData, - rectangle, - width, - result, - ) { + static packPolylineSegments(collection, collectionData, rectangle, result) { result.segments ??= []; result.widths ??= []; result.colors ??= []; result.segmentPrimitiveIndices ??= []; result.primitiveCount ??= 0; + const width = rectangle.width; const primitiveCount = collection.primitiveCount; const positions = collectionData.positions; @@ -397,22 +391,16 @@ class VectorPipeline { * @param {BufferPolygonCollection} collection * @param {VectorCollectionData} collectionData * @param {Rectangle} rectangle - * @param {number} width * @param {VectorTileData} result */ - static packPolygonRings( - collection, - collectionData, - rectangle, - width, - result, - ) { + static packPolygonRings(collection, collectionData, rectangle, result) { result.polygonRings ??= []; result.polygonRingPrimitiveIndices ??= []; result.widths ??= []; result.colors ??= []; result.primitiveCount ??= 0; + const width = rectangle.width; const primitiveCount = collection.primitiveCount; const positions = collectionData.positions; diff --git a/packages/engine/Source/Core/VectorProvider.js b/packages/engine/Source/Core/VectorProvider.js index 435ad4070da..003c65450a6 100644 --- a/packages/engine/Source/Core/VectorProvider.js +++ b/packages/engine/Source/Core/VectorProvider.js @@ -37,7 +37,6 @@ const scratchIntersectRectangle = new Rectangle(); * @param {*} collection * @param {VectorCollectionData} collectionData * @param {Rectangle} rectangle - * @param {number} width * @param {VectorTileData} result * @returns {void} * @private @@ -222,7 +221,6 @@ class VectorProvider { requestTileData(x, y, level, context) { const tilingScheme = this._tilingScheme; const tileRectangle = tilingScheme.tileXYToRectangle(x, y, level); - const width = Rectangle.computeWidth(tileRectangle); /** @type {VectorTileData} */ const result = { show: true }; @@ -257,7 +255,6 @@ class VectorProvider { collection, collectionData, tileRectangle, - width, result, ); } diff --git a/packages/engine/Source/Shaders/VectorCommon.glsl b/packages/engine/Source/Shaders/VectorCommon.glsl index 9af4f254ac6..f09c2df61fc 100644 --- a/packages/engine/Source/Shaders/VectorCommon.glsl +++ b/packages/engine/Source/Shaders/VectorCommon.glsl @@ -52,6 +52,9 @@ vec4 vectorPolylineRender(vec2 vectorUv, vec4 baseColor) int cellY = clamp(int(vectorUv.y * float(gridHeight)), 0, gridHeight - 1); int cellIndex = cellX + cellY * gridWidth; + // Cell end offsets follow the two gridWidth/gridHeight texels, so cell + // N's end is at texel N + 2. A cell's start is the previous cell's end + // (texel N + 1); cell 0's start is implicitly 0. int indexEnd = int(texelFetch(u_vectorGridCellIndicesTexture, vectorIndexToUv(cellIndex + 2, headerSize), 0).r); int indexStart = cellIndex == 0 ? 0 @@ -83,6 +86,35 @@ vec4 vectorPolylineRender(vec2 vectorUv, vec4 baseColor) return baseColor; } +// Composites a polygon's fill over baseColor when the pixel is inside it. A +// negative index (empty cell or first iteration) or an outside pixel is a +// no-op. +vec4 vectorCompositePolygonFill(vec4 baseColor, int primitiveIndex, bool inside, ivec2 primitiveTextureSize) +{ + if (!inside || primitiveIndex < 0) + { + return baseColor; + } + + ivec2 primitiveUv = vectorIndexToUv(primitiveIndex, primitiveTextureSize); + vec4 fillColor = texelFetch(u_vectorColorTexture, primitiveUv, 0); + return fillColor * vec4(fillColor.aaa, 1.0) + baseColor * (1.0 - fillColor.a); +} + +// True if a horizontal +x ray from p crosses the edge. The half-open interval +// (> vs <=) counts a ray through a shared vertex exactly once. +bool vectorEdgeCrossesRay(vec4 edge, vec2 p) +{ + if ((edge.y > p.y) == (edge.w > p.y)) + { + return false; + } + + float t = (p.y - edge.y) / (edge.w - edge.y); + float xIntersect = edge.x + t * (edge.z - edge.x); + return p.x < xIntersect; +} + // Drape clamped vector polygon fills onto the terrain surface. The fragment's // tile UV picks a grid cell whose edges were clipped to the cell on the CPU, // forming closed loops, so an even-odd horizontal ray cast within the cell @@ -104,6 +136,9 @@ vec4 vectorPolygonRender(vec2 vectorUv, vec4 baseColor) int cellY = clamp(int(vectorUv.y * float(gridHeight)), 0, gridHeight - 1); int cellIndex = cellX + cellY * gridWidth; + // Cell end offsets follow the two gridWidth/gridHeight texels, so cell + // N's end is at texel N + 2. A cell's start is the previous cell's end + // (texel N + 1); cell 0's start is implicitly 0. int indexEnd = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, vectorIndexToUv(cellIndex + 2, headerSize), 0).r); int indexStart = cellIndex == 0 ? 0 @@ -115,42 +150,29 @@ vec4 vectorPolygonRender(vec2 vectorUv, vec4 baseColor) int currentPrimitive = -1; bool inside = false; - // One extra iteration (i == indexEnd) flushes the final primitive group. - for (int i = indexStart; i <= indexEnd; i++) + for (int i = indexStart; i < indexEnd; i++) { - int primitiveIndex = -1; - vec4 edge = vec4(0.0); - if (i < indexEnd) - { - ivec2 edgeUv = vectorIndexToUv(i, edgeTextureSize); - edge = texelFetch(u_vectorPolygonEdgeTexture, edgeUv, 0); - primitiveIndex = int(texelFetch(u_vectorPolygonEdgePrimitiveIndicesTexture, edgeUv, 0).r); - } + ivec2 edgeUv = vectorIndexToUv(i, edgeTextureSize); + vec4 edge = texelFetch(u_vectorPolygonEdgeTexture, edgeUv, 0); + int primitiveIndex = int(texelFetch(u_vectorPolygonEdgePrimitiveIndicesTexture, edgeUv, 0).r); + // A new primitive means the previous group is complete: composite it, + // then start counting the new one fresh. if (primitiveIndex != currentPrimitive) { - if (currentPrimitive >= 0 && inside) - { - ivec2 primitiveUv = vectorIndexToUv(currentPrimitive, primitiveTextureSize); - vec4 fillColor = texelFetch(u_vectorColorTexture, primitiveUv, 0); - baseColor = fillColor * vec4(fillColor.aaa, 1.0) + baseColor * (1.0 - fillColor.a); - } + baseColor = vectorCompositePolygonFill(baseColor, currentPrimitive, inside, primitiveTextureSize); currentPrimitive = primitiveIndex; inside = false; } - // Even-odd rule with a horizontal +x ray; the half-open interval - // (> vs <=) counts a ray through a shared vertex exactly once. - if (i < indexEnd && (edge.y > vectorUv.y) != (edge.w > vectorUv.y)) + if (vectorEdgeCrossesRay(edge, vectorUv)) { - float t = (vectorUv.y - edge.y) / (edge.w - edge.y); - float xIntersect = edge.x + t * (edge.z - edge.x); - if (vectorUv.x < xIntersect) - { - inside = !inside; - } + inside = !inside; } } + // The last primitive group has no trailing edge to trigger its composite. + baseColor = vectorCompositePolygonFill(baseColor, currentPrimitive, inside, primitiveTextureSize); + return baseColor; }