| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134 |
- import Cartesian2 from "../Core/Cartesian2.js";
- import CesiumMath from "../Core/Math.js";
- import Check from "../Core/Check.js";
- import Frozen from "../Core/Frozen.js";
- import defined from "../Core/defined.js";
- import destroyObject from "../Core/destroyObject.js";
- import DeveloperError from "../Core/DeveloperError.js";
- import Event from "../Core/Event.js";
- import Intersect from "../Core/Intersect.js";
- import PixelFormat from "../Core/PixelFormat.js";
- import Rectangle from "../Core/Rectangle.js";
- import ContextLimits from "../Renderer/ContextLimits.js";
- import PixelDatatype from "../Renderer/PixelDatatype.js";
- import RuntimeError from "../Core/RuntimeError.js";
- import Sampler from "../Renderer/Sampler.js";
- import Texture from "../Renderer/Texture.js";
- import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js";
- import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js";
- import TextureWrap from "../Renderer/TextureWrap.js";
- import ClippingPolygon from "./ClippingPolygon.js";
- import ComputeCommand from "../Renderer/ComputeCommand.js";
- import PolygonSignedDistanceFS from "../Shaders/PolygonSignedDistanceFS.js";
- import Pass from "../Renderer/Pass.js";
-
- /**
- * Specifies a set of clipping polygons. Clipping polygons selectively disable rendering in a region
- * inside or outside the specified list of {@link ClippingPolygon} objects for a single glTF model, 3D Tileset, or the globe.
- *
- * Clipping Polygons are only supported in WebGL 2 contexts.
- *
- * @alias ClippingPolygonCollection
- * @constructor
- *
- * @param {object} [options] Object with the following properties:
- * @param {ClippingPolygon[]} [options.polygons=[]] An array of {@link ClippingPolygon} objects used to selectively disable rendering on the inside of each polygon.
- * @param {boolean} [options.enabled=true] Determines whether the clipping polygons are active.
- * @param {boolean} [options.inverse=false] If true, a region will be clipped if it is outside of every polygon in the collection. Otherwise, a region will only be clipped if it is on the inside of any polygon.
- * @param {number} [options.quality=1.0] A scalar that controls the resolution of the signed distance texture used for clipping. Values greater than 1.0 increase quality, values less than 1.0 decrease it. Must be greater than 0.0.
- *
- * @example
- * const positions = Cesium.Cartesian3.fromRadiansArray([
- * -1.3194369277314022,
- * 0.6988062530900625,
- * -1.31941,
- * 0.69879,
- * -1.3193955980204217,
- * 0.6988091578771254,
- * -1.3193931220959367,
- * 0.698743632490865,
- * -1.3194358224045408,
- * 0.6987471965556998,
- * ]);
- *
- * const polygon = new Cesium.ClippingPolygon({
- * positions: positions
- * });
- *
- * const polygons = new Cesium.ClippingPolygonCollection({
- * polygons: [ polygon ]
- * });
- */
- function ClippingPolygonCollection(options) {
- options = options ?? Frozen.EMPTY_OBJECT;
-
- this._polygons = [];
- this._totalPositions = 0;
-
- this.debugShowDistanceTexture = options.debugShowDistanceTexture ?? false;
-
- /**
- * If true, clipping will be enabled.
- *
- * @type {boolean}
- * @default true
- */
- this.enabled = options.enabled ?? true;
-
- /**
- * If true, a region will be clipped if it is outside of every polygon in the
- * collection. Otherwise, a region will only be clipped if it is
- * inside of any polygon.
- *
- * @type {boolean}
- * @default false
- */
- this.inverse = options.inverse ?? false;
-
- /**
- * A scalar that controls the resolution of the signed distance texture used for clipping.
- * Values greater than 1.0 increase quality, values less than 1.0 decrease it. Must be greater than 0.0.
- *
- * @type {number}
- * @default 1.0
- */
- this.quality = options.quality ?? 1.0;
-
- /**
- * An event triggered when a new clipping polygon is added to the collection. Event handlers
- * are passed the new polygon and the index at which it was added.
- * @type {Event}
- * @default Event()
- */
- this.polygonAdded = new Event();
-
- /**
- * An event triggered when a new clipping polygon is removed from the collection. Event handlers
- * are passed the new polygon and the index from which it was removed.
- * @type {Event}
- * @default Event()
- */
- this.polygonRemoved = new Event();
-
- // If this ClippingPolygonCollection has an owner, only its owner should update or destroy it.
- // This is because in a Cesium3DTileset multiple models may reference the tileset's ClippingPolygonCollection.
- this._owner = undefined;
-
- this._float32View = undefined;
- this._extentsFloat32View = undefined;
- this._extentsCount = 0;
-
- this._polygonsTexture = undefined;
- this._extentsTexture = undefined;
- this._signedDistanceTexture = undefined;
-
- this._signedDistanceComputeCommand = undefined;
-
- // Add each ClippingPolygon object.
- const polygons = options.polygons;
- if (defined(polygons)) {
- const polygonsLength = polygons.length;
- for (let i = 0; i < polygonsLength; ++i) {
- this._polygons.push(polygons[i]);
- }
- }
- }
-
- Object.defineProperties(ClippingPolygonCollection.prototype, {
- /**
- * Returns the number of polygons in this collection. This is commonly used with
- * {@link ClippingPolygonCollection#get} to iterate over all the polygons
- * in the collection.
- *
- * @memberof ClippingPolygonCollection.prototype
- * @type {number}
- * @readonly
- */
- length: {
- get: function () {
- return this._polygons.length;
- },
- },
-
- /**
- * Returns the total number of positions in all polygons in the collection.
- *
- * @memberof ClippingPolygonCollection.prototype
- * @type {number}
- * @readonly
- * @private
- */
- totalPositions: {
- get: function () {
- return this._totalPositions;
- },
- },
-
- /**
- * Returns a texture containing the packed computed spherical extents for each polygon
- *
- * @memberof ClippingPolygonCollection.prototype
- * @type {Texture}
- * @readonly
- * @private
- */
- extentsTexture: {
- get: function () {
- return this._extentsTexture;
- },
- },
-
- /**
- * Returns the number of packed extents, which can be fewer than the number of polygons.
- *
- * @memberof ClippingPolygonCollection.prototype
- * @type {number}
- * @readonly
- * @private
- */
- extentsCount: {
- get: function () {
- return this._extentsCount;
- },
- },
-
- /**
- * Returns the number of pixels needed in the texture containing the packed computed spherical extents for each polygon.
- *
- * @memberof ClippingPolygonCollection.prototype
- * @type {number}
- * @readonly
- * @private
- */
- pixelsNeededForExtents: {
- get: function () {
- return this.length; // With an RGBA texture, each pixel contains min/max latitude and longitude.
- },
- },
-
- /**
- * Returns the number of pixels needed in the texture containing the packed polygon positions.
- *
- * @memberof ClippingPolygonCollection.prototype
- * @type {number}
- * @readonly
- * @private
- */
- pixelsNeededForPolygonPositions: {
- get: function () {
- // In an RG FLOAT texture, each polygon position is 2 floats packed to a RG.
- // Each polygon has a 1-pixel header + 2 pixels for individual extents + the list of positions
- return this.totalPositions + 3 * this.length;
- },
- },
-
- /**
- * Returns a texture containing the computed signed distance of each polygon.
- *
- * @memberof ClippingPolygonCollection.prototype
- * @type {Texture}
- * @readonly
- * @private
- */
- clippingTexture: {
- get: function () {
- return this._signedDistanceTexture;
- },
- },
-
- /**
- * A reference to the ClippingPolygonCollection's owner, if any.
- *
- * @memberof ClippingPolygonCollection.prototype
- * @readonly
- * @private
- */
- owner: {
- get: function () {
- return this._owner;
- },
- },
-
- /**
- * Returns a number encapsulating the state for this ClippingPolygonCollection.
- *
- * Clipping mode is encoded in the sign of the number, which is just the total position count.
- * If this value changes, then shader regeneration is necessary.
- *
- * @memberof ClippingPolygonCollection.prototype
- * @returns {number} A Number that describes the ClippingPolygonCollection's state.
- * @readonly
- * @private
- */
- clippingPolygonsState: {
- get: function () {
- return this.inverse ? -this.extentsCount : this.extentsCount;
- },
- },
- });
-
- /**
- * Adds the specified {@link ClippingPolygon} to the collection to be used to selectively disable rendering
- * on the inside of each polygon. Use {@link ClippingPolygonCollection#unionClippingRegions} to modify
- * how modify the clipping behavior of multiple polygons.
- *
- * @param {ClippingPolygon} polygon The ClippingPolygon to add to the collection.
- * @returns {ClippingPolygon} The added ClippingPolygon.
- *
- * @example
- * const polygons = new Cesium.ClippingPolygonCollection();
- *
- * const positions = Cesium.Cartesian3.fromRadiansArray([
- * -1.3194369277314022,
- * 0.6988062530900625,
- * -1.31941,
- * 0.69879,
- * -1.3193955980204217,
- * 0.6988091578771254,
- * -1.3193931220959367,
- * 0.698743632490865,
- * -1.3194358224045408,
- * 0.6987471965556998,
- * ]);
- *
- * polygons.add(new Cesium.ClippingPolygon({
- * positions: positions
- * }));
- *
- *
- *
- * @see ClippingPolygonCollection#remove
- * @see ClippingPolygonCollection#removeAll
- */
- ClippingPolygonCollection.prototype.add = function (polygon) {
- //>>includeStart('debug', pragmas.debug);
- Check.typeOf.object("polygon", polygon);
- //>>includeEnd('debug');
-
- const newPlaneIndex = this._polygons.length;
- this._polygons.push(polygon);
- this.polygonAdded.raiseEvent(polygon, newPlaneIndex);
- return polygon;
- };
-
- /**
- * Returns the clipping polygon in the collection at the specified index. Indices are zero-based
- * and increase as polygons are added. Removing a polygon polygon all polygons after
- * it to the left, changing their indices. This function is commonly used with
- * {@link ClippingPolygonCollection#length} to iterate over all the polygons
- * in the collection.
- *
- * @param {number} index The zero-based index of the polygon.
- * @returns {ClippingPolygon} The ClippingPolygon at the specified index.
- *
- * @see ClippingPolygonCollection#length
- */
- ClippingPolygonCollection.prototype.get = function (index) {
- //>>includeStart('debug', pragmas.debug);
- Check.typeOf.number("index", index);
- //>>includeEnd('debug');
-
- return this._polygons[index];
- };
-
- /**
- * Checks whether this collection contains a ClippingPolygon equal to the given ClippingPolygon.
- *
- * @param {ClippingPolygon} polygon The ClippingPolygon to check for.
- * @returns {boolean} true if this collection contains the ClippingPolygon, false otherwise.
- *
- * @see ClippingPolygonCollection#get
- */
- ClippingPolygonCollection.prototype.contains = function (polygon) {
- //>>includeStart('debug', pragmas.debug);
- Check.typeOf.object("polygon", polygon);
- //>>includeEnd('debug');
-
- return this._polygons.some((p) => ClippingPolygon.equals(p, polygon));
- };
-
- /**
- * Removes the first occurrence of the given ClippingPolygon from the collection.
- *
- * @param {ClippingPolygon} polygon
- * @returns {boolean} <code>true</code> if the polygon was removed; <code>false</code> if the polygon was not found in the collection.
- *
- * @see ClippingPolygonCollection#add
- * @see ClippingPolygonCollection#contains
- * @see ClippingPolygonCollection#removeAll
- */
- ClippingPolygonCollection.prototype.remove = function (polygon) {
- //>>includeStart('debug', pragmas.debug);
- Check.typeOf.object("polygon", polygon);
- //>>includeEnd('debug');
-
- const polygons = this._polygons;
- const index = polygons.findIndex((p) => ClippingPolygon.equals(p, polygon));
-
- if (index === -1) {
- return false;
- }
-
- polygons.splice(index, 1);
-
- this.polygonRemoved.raiseEvent(polygon, index);
- return true;
- };
-
- /**
- * Computes padded extents for a polygon's bounding rectangle, clamped to valid spherical ranges.
- *
- * @param {Rectangle} extents The original spherical extents to pad.
- * @param {number} padding A multiplier applied to the extents' width and height to determine the padding amount.
- * @param {Rectangle} [result] An optional rectangle to store the result in.
- * @returns {Rectangle} The padded and clamped rectangle.
- *
- * @private
- */
- function computePaddedExtents(extents, padding, result) {
- const height = Math.max(extents.height * padding, 0);
- const width = Math.max(extents.width * padding, 0);
- const paddedExtents = Rectangle.clone(extents, result);
-
- // Pad
- paddedExtents.south -= height;
- paddedExtents.west -= width;
- paddedExtents.north += height;
- paddedExtents.east += width;
-
- // Clamp
- paddedExtents.south = Math.max(paddedExtents.south, -Math.PI);
- paddedExtents.west = Math.max(paddedExtents.west, -Math.PI);
- paddedExtents.north = Math.min(paddedExtents.north, Math.PI);
- paddedExtents.east = Math.min(paddedExtents.east, Math.PI);
-
- return paddedExtents;
- }
-
- /**
- * @typedef {object} ExtentsResult
- * @property {Rectangle[]} extentsList The list of merged padded extents, one per group.
- * @property {Map<number, number>} extentsIndexByPolygon A map from polygon index to the index of its group in extentsList.
- * @private
- */
-
- /**
- * Groups nearby ClippingPolygons based on their spherical extents. Overlapping extents will be merged
- * into a single encompassing extent. Each Extent will later map into one region in the SignedDistanceTexture (atlas).
- *
- * Definitions:
- * n = number of polygons
- * g = number of resulting extents (merged) (g <= n)
- * absorb = merge two extents into one
- * restart = redo intersection check with previous groups
- *
- * Algorithm:
- * For each polygon we scan existing groups for a first overlap (O(g)),
- * then on each subsequent overlap we absorb the group and restart the
- * inner scan. Each group can be absorbed at most once per polygon, and
- * each restart reduces the group count by one, so the absorb-loop does
- * at most O(g) restarts per polygon. Overall: O(n * g) where g ≤ n,
- * giving O(n²) worst case when all polygons overlap transitively, but
- * typically much better when groups are few and disjoint.
- *
- * Note: Restarts are required because the new merged bounding box might
- * be larger than the two individual that were merged and introduce new
- * collisions. Example:
- *
- * Before merging A and B:
- *
- * ┌─────────┐
- * │ A │
- * │ ┌┼────────┐
- * └─────────┘│ B │
- * ┌────┐ │ │
- * │ C │ └────────┘
- * └────┘
- *
- * A overlaps B ✓
- * A overlaps C ✗
- * B overlaps C ✗
- *
- * After merging A ∪ B into one extent:
- *
- * ┌───────────────────┐
- * │ │
- * │ A ∪ B │
- * ├────┐ │
- * │ C │ │
- * └────┘──────────────┘
- *
- * (A ∪ B) overlaps C ✓ ← new collision!
- *
- * @param {ClippingPolygon[]} polygons The array of clipping polygons to compute extents for.
- * @param {Rectangle[]} polygonExtentsCache An array of pre-computed spherical extents for each polygon, indexed by polygon index.
- * @returns {ExtentsResult} The merged extents and a mapping from polygon indices to their extent group indices.
- *
- * @private
- */
- function getExtents(polygons, polygonExtentsCache) {
- // Pad extents to avoid floating point error when fragment culling at edges.
- const PADDING = 2.5;
-
- // Each group: { extent: padded Rectangle, polygonIndices: number[] }
- const groups = [];
-
- const length = polygons.length;
- for (let polygonIndex = 0; polygonIndex < length; ++polygonIndex) {
- const paddedExtent = computePaddedExtents(
- polygonExtentsCache[polygonIndex],
- PADDING,
- );
-
- // Pass 1: Find the first overlapping group
- let targetIdx = -1;
- for (let g = 0; g < groups.length; ++g) {
- if (
- defined(Rectangle.simpleIntersection(groups[g].extent, paddedExtent))
- ) {
- targetIdx = g;
- break;
- }
- }
-
- if (targetIdx === -1) {
- // No overlap — start a new group
- groups.push({ extent: paddedExtent, polygonIndices: [polygonIndex] });
- } else {
- // Overlap - Merge the polygon into the target group
- const target = groups[targetIdx];
- target.polygonIndices.push(polygonIndex);
- Rectangle.union(target.extent, paddedExtent, target.extent);
-
- // Pass 2: Absorb all other groups that overlap the (growing) target
- // extent. After each absorption the target grows, so restart the scan
- // to catch groups that now transitively overlap.
- for (let g = 0; g < groups.length; ++g) {
- if (g === targetIdx) {
- continue;
- }
- if (
- defined(Rectangle.simpleIntersection(groups[g].extent, target.extent))
- ) {
- target.polygonIndices.push(...groups[g].polygonIndices);
- Rectangle.union(target.extent, groups[g].extent, target.extent);
- groups.splice(g, 1);
- if (g < targetIdx) {
- targetIdx--;
- }
- g = -1; // restart (loop increment brings it to 0)
- }
- }
- }
- }
-
- const extentsList = groups.map((g) => g.extent);
- const extentsIndexByPolygon = new Map();
- groups.forEach((g, extentIndex) =>
- g.polygonIndices.forEach((p) => extentsIndexByPolygon.set(p, extentIndex)),
- );
-
- return { extentsList, extentsIndexByPolygon };
- }
-
- /**
- * Removes all polygons from the collection.
- *
- * @see ClippingPolygonCollection#add
- * @see ClippingPolygonCollection#remove
- */
- ClippingPolygonCollection.prototype.removeAll = function () {
- // Dereference this ClippingPolygonCollection from all ClippingPolygons
- const polygons = this._polygons;
- const polygonsCount = polygons.length;
- for (let i = 0; i < polygonsCount; ++i) {
- const polygon = polygons[i];
- this.polygonRemoved.raiseEvent(polygon, i);
- }
- this._polygons = [];
- };
-
- function packPolygonsAsFloats(clippingPolygonCollection) {
- const polygonsFloat32View = clippingPolygonCollection._float32View;
- const extentsFloat32View = clippingPolygonCollection._extentsFloat32View;
- const polygons = clippingPolygonCollection._polygons;
-
- /**
- * Pre-calculate all polygon spherical extents as it an expensive operation
- * @type {ReadonlyArray<Rectangle>}
- * */
- const polygonExtentsCache = polygons.map((polygon) =>
- polygon.computeSphericalExtents(),
- );
-
- const { extentsList, extentsIndexByPolygon } = getExtents(
- polygons,
- polygonExtentsCache,
- );
-
- // Polygons are packed sequentially (ordered by extentsIndex) into polygonsFloat32View as follows:
- // For each polygon:
- // [0] vertexCount - the number of vertices in the polygon
- // [1] extentsIndex - index into the extents texture for this polygon's bounding rectangle
- // [2] south - southern boundary of the individual polygon extent (radians)
- // [3] west - western boundary of the individual polygon extent (radians)
- // [4] latitudeRange - (north - south) for the individual polygon extent
- // [5] longitudeRange - (east - west) for the individual polygon extent
- // [6..6+2*vertexCount-1] pairs of (latitude, longitude) for each vertex,
- // computed as fastApproximateAtan2 values to match the shader
-
- // Sort polygon indices by extentsIndex so polygons sharing the same extent are packed together
- // Can enable optimizations in the shader
- const sortedPolygonIndices = Array.from(polygons.keys()).sort(
- (a, b) => extentsIndexByPolygon.get(a) - extentsIndexByPolygon.get(b),
- );
-
- let floatIndex = 0;
- for (const polygonIndex of sortedPolygonIndices) {
- const polygon = polygons[polygonIndex];
- // Pack the length of the polygon into the polygon texture array buffer
- const length = polygon.length;
- polygonsFloat32View[floatIndex++] = length;
- polygonsFloat32View[floatIndex++] = extentsIndexByPolygon.get(polygonIndex);
-
- // Pack the individual polygon extent
- const polygonExtent = polygonExtentsCache[polygonIndex];
- polygonsFloat32View[floatIndex++] = polygonExtent.south;
- polygonsFloat32View[floatIndex++] = polygonExtent.west;
- polygonsFloat32View[floatIndex++] =
- polygonExtent.north - polygonExtent.south;
- polygonsFloat32View[floatIndex++] = polygonExtent.east - polygonExtent.west;
-
- // Pack the polygon positions into the polygon texture array buffer
- for (let i = 0; i < length; ++i) {
- const spherePoint = polygon.positions[i];
-
- // Project into plane with vertical for latitude
- const magXY = Math.hypot(spherePoint.x, spherePoint.y);
-
- // Use fastApproximateAtan2 for alignment with shader
- const latitudeApproximation = CesiumMath.fastApproximateAtan2(
- magXY,
- spherePoint.z,
- );
- const longitudeApproximation = CesiumMath.fastApproximateAtan2(
- spherePoint.x,
- spherePoint.y,
- );
-
- polygonsFloat32View[floatIndex++] = latitudeApproximation;
- polygonsFloat32View[floatIndex++] = longitudeApproximation;
- }
- }
-
- // Extents are packed sequentially into extentsFloat32View as follows:
- // For each extent (maps to one RGBA pixel in the extents texture):
- // [0] south - the southern boundary of the bounding rectangle (radians)
- // [1] west - the western boundary of the bounding rectangle (radians)
- // [2] latitudeRangeInverse - 1.0 / (north - south)
- // [3] longitudeRangeInverse - 1.0 / (east - west)
- let extentsFloatIndex = 0;
- for (const extents of extentsList) {
- const longitudeRangeInverse = 1.0 / (extents.east - extents.west);
- const latitudeRangeInverse = 1.0 / (extents.north - extents.south);
-
- extentsFloat32View[extentsFloatIndex++] = extents.south;
- extentsFloat32View[extentsFloatIndex++] = extents.west;
- extentsFloat32View[extentsFloatIndex++] = latitudeRangeInverse;
- extentsFloat32View[extentsFloatIndex++] = longitudeRangeInverse;
- }
-
- clippingPolygonCollection._extentsCount = extentsList.length;
- }
-
- const textureResolutionScratch = new Cartesian2();
- /**
- * Called when {@link Viewer} or {@link CesiumWidget} render the scene to
- * build the resources for clipping polygons.
- * <p>
- * Do not call this function directly.
- * </p>
- * @private
- * @throws {RuntimeError} ClippingPolygonCollections are only supported for WebGL 2
- */
- ClippingPolygonCollection.prototype.update = function (frameState) {
- const context = frameState.context;
-
- if (!ClippingPolygonCollection.isSupported(frameState)) {
- throw new RuntimeError(
- "ClippingPolygonCollections are only supported for WebGL 2.",
- );
- }
-
- if (this.debugShowDistanceTexture && defined(this._signedDistanceTexture)) {
- if (!defined(this.debugCommand)) {
- this.debugCommand = createDebugCommand(
- this._signedDistanceTexture,
- frameState.context,
- );
- }
- frameState.commandList.push(this.debugCommand);
- }
-
- // It'd be expensive to validate any individual position has changed. Instead verify if the list of polygon positions has had elements added or removed, which should be good enough for most cases.
- const totalPositions = this._polygons.reduce(
- (totalPositions, polygon) => totalPositions + polygon.length,
- 0,
- );
-
- if (totalPositions === this.totalPositions) {
- return;
- }
-
- this._totalPositions = totalPositions;
-
- // If there are no clipping polygons, there's nothing to update.
- if (this.length === 0) {
- return;
- }
-
- if (defined(this._signedDistanceComputeCommand)) {
- this._signedDistanceComputeCommand.canceled = true;
- this._signedDistanceComputeCommand = undefined;
- }
-
- let polygonsTexture = this._polygonsTexture;
- let extentsTexture = this._extentsTexture;
- let signedDistanceTexture = this._signedDistanceTexture;
- if (defined(polygonsTexture)) {
- const currentPixelCount = polygonsTexture.width * polygonsTexture.height;
- // Recreate the texture to double current requirement if it isn't big enough or is 4 times larger than it needs to be.
- // Optimization note: this isn't exactly the classic resizeable array algorithm
- // * not necessarily checking for resize after each add/remove operation
- // * random-access deletes instead of just pops
- // * alloc ops likely more expensive than demonstrable via big-O analysis
- if (
- currentPixelCount < this.pixelsNeededForPolygonPositions ||
- this.pixelsNeededForPolygonPositions < 0.25 * currentPixelCount
- ) {
- polygonsTexture.destroy();
- polygonsTexture = undefined;
- this._polygonsTexture = undefined;
- }
- }
-
- if (!defined(polygonsTexture)) {
- const requiredResolution = ClippingPolygonCollection.getTextureResolution(
- polygonsTexture,
- this.pixelsNeededForPolygonPositions,
- textureResolutionScratch,
- );
-
- polygonsTexture = new Texture({
- context: context,
- width: requiredResolution.x,
- height: requiredResolution.y,
- pixelFormat: PixelFormat.RG,
- pixelDatatype: PixelDatatype.FLOAT,
- sampler: Sampler.NEAREST,
- flipY: false,
- });
- this._float32View = new Float32Array(
- requiredResolution.x * requiredResolution.y * 2,
- );
- this._polygonsTexture = polygonsTexture;
- }
-
- if (defined(extentsTexture)) {
- const currentPixelCount = extentsTexture.width * extentsTexture.height;
- // Recreate the texture to double current requirement if it isn't big enough or is 4 times larger than it needs to be.
- // Optimization note: this isn't exactly the classic resizeable array algorithm
- // * not necessarily checking for resize after each add/remove operation
- // * random-access deletes instead of just pops
- // * alloc ops likely more expensive than demonstrable via big-O analysis
- if (
- currentPixelCount < this.pixelsNeededForExtents ||
- this.pixelsNeededForExtents < 0.25 * currentPixelCount
- ) {
- extentsTexture.destroy();
- extentsTexture = undefined;
- this._extentsTexture = undefined;
- }
- }
-
- if (!defined(extentsTexture)) {
- const requiredResolution = ClippingPolygonCollection.getTextureResolution(
- extentsTexture,
- this.pixelsNeededForExtents,
- textureResolutionScratch,
- );
-
- extentsTexture = new Texture({
- context: context,
- width: requiredResolution.x,
- height: requiredResolution.y,
- pixelFormat: PixelFormat.RGBA,
- pixelDatatype: PixelDatatype.FLOAT,
- sampler: Sampler.NEAREST,
- flipY: false,
- });
- this._extentsFloat32View = new Float32Array(
- requiredResolution.x * requiredResolution.y * 4,
- );
-
- this._extentsTexture = extentsTexture;
- }
-
- packPolygonsAsFloats(this);
-
- extentsTexture.copyFrom({
- source: {
- width: extentsTexture.width,
- height: extentsTexture.height,
- arrayBufferView: this._extentsFloat32View,
- },
- });
-
- polygonsTexture.copyFrom({
- source: {
- width: polygonsTexture.width,
- height: polygonsTexture.height,
- arrayBufferView: this._float32View,
- },
- });
-
- if (!defined(signedDistanceTexture)) {
- const textureDimensions =
- ClippingPolygonCollection.getClippingDistanceTextureResolution(
- this,
- textureResolutionScratch,
- );
- signedDistanceTexture = new Texture({
- context: context,
- width: textureDimensions.x,
- height: textureDimensions.y,
- pixelFormat: context.webgl2 ? PixelFormat.RED : PixelFormat.LUMINANCE,
- pixelDatatype: PixelDatatype.FLOAT,
- sampler: new Sampler({
- wrapS: TextureWrap.CLAMP_TO_EDGE,
- wrapT: TextureWrap.CLAMP_TO_EDGE,
- minificationFilter: TextureMinificationFilter.LINEAR,
- magnificationFilter: TextureMagnificationFilter.LINEAR,
- }),
- flipY: false,
- });
-
- this._signedDistanceTexture = signedDistanceTexture;
- }
-
- this._signedDistanceComputeCommand = createSignedDistanceTextureCommand(this);
- };
-
- function createDebugCommand(texture, context) {
- const fs =
- "uniform highp sampler2D billboard_texture; \n" +
- "in vec2 v_textureCoordinates; \n" +
- "float getSignedDistance(vec2 uv, highp sampler2D clippingDistance) { \n" +
- " float signedDistance = texture(clippingDistance, uv).r; \n" +
- " return (signedDistance - 0.5) * 2.0; \n" +
- "} \n" +
- "void main() \n" +
- "{ \n" +
- " float dist = texture(billboard_texture, v_textureCoordinates).r; \n" +
- " if (dist > 0.5) { \n" +
- " out_FragColor = vec4(dist, 0.0, 0.0, 1.0); \n" + // outside
- " } else {\n" +
- " out_FragColor = vec4(0.0, dist, 0.0, 1.0); \n" + // inside
- " } \n" +
- "} \n";
-
- const drawCommand = context.createViewportQuadCommand(fs, {
- uniformMap: {
- billboard_texture: function () {
- return texture;
- },
- },
- });
- drawCommand.pass = Pass.OVERLAY;
- return drawCommand;
- }
-
- /**
- * Called when {@link Viewer} or {@link CesiumWidget} render the scene to
- * build the resources for clipping polygons.
- * <p>
- * Do not call this function directly.
- * </p>
- * @private
- * @param {FrameState} frameState
- */
- ClippingPolygonCollection.prototype.queueCommands = function (frameState) {
- if (defined(this._signedDistanceComputeCommand)) {
- frameState.commandList.push(this._signedDistanceComputeCommand);
- }
- };
-
- function createSignedDistanceTextureCommand(collection) {
- const polygonTexture = collection._polygonsTexture;
- const extentsTexture = collection._extentsTexture;
-
- return new ComputeCommand({
- fragmentShaderSource: PolygonSignedDistanceFS,
- outputTexture: collection._signedDistanceTexture,
- uniformMap: {
- u_polygonsLength: function () {
- return collection.length;
- },
- u_extentsLength: function () {
- return collection.extentsCount;
- },
- u_extentsTexture: function () {
- return extentsTexture;
- },
- u_polygonTexture: function () {
- return polygonTexture;
- },
- },
- persists: false,
- owner: collection,
- postExecute: () => {
- collection._signedDistanceComputeCommand = undefined;
- },
- });
- }
-
- const scratchRectangleTile = new Rectangle();
- const scratchRectangleIntersection = new Rectangle();
- const scratchRectanglePolygon = new Rectangle();
- /**
- * Determines the type intersection with the polygons of this ClippingPolygonCollection instance and the specified {@link TileBoundingVolume}.
- * @private
- *
- * @param {object} tileBoundingVolume The volume to determine the intersection with the polygons.
- * @param {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid on which the bounding volumes are defined.
- * @returns {Intersect} The intersection type: {@link Intersect.OUTSIDE} if the entire volume is not clipped, {@link Intersect.INSIDE}
- * if the entire volume should be clipped, and {@link Intersect.INTERSECTING} if the volume intersects the polygons and will partially clipped.
- */
- ClippingPolygonCollection.prototype.computeIntersectionWithBoundingVolume =
- function (tileBoundingVolume, ellipsoid) {
- const polygons = this._polygons;
- const length = polygons.length;
-
- let intersection = Intersect.OUTSIDE;
- if (this.inverse) {
- intersection = Intersect.INSIDE;
- }
-
- let tileBoundingRectangle = tileBoundingVolume.rectangle;
- if (
- !defined(tileBoundingRectangle) &&
- defined(tileBoundingVolume.boundingVolume?.computeCorners)
- ) {
- const points = tileBoundingVolume.boundingVolume.computeCorners();
- tileBoundingRectangle = Rectangle.fromCartesianArray(
- points,
- ellipsoid,
- scratchRectangleTile,
- );
- }
-
- if (!defined(tileBoundingRectangle)) {
- tileBoundingRectangle = Rectangle.fromBoundingSphere(
- tileBoundingVolume.boundingSphere,
- ellipsoid,
- scratchRectangleTile,
- );
- }
-
- for (let i = 0; i < length; ++i) {
- const polygon = polygons[i];
-
- const polygonBoundingRectangle = polygon.computeRectangle(
- scratchRectanglePolygon,
- );
-
- const result = Rectangle.simpleIntersection(
- tileBoundingRectangle,
- polygonBoundingRectangle,
- scratchRectangleIntersection,
- );
-
- if (defined(result)) {
- return Intersect.INTERSECTING;
- }
- }
-
- return intersection;
- };
-
- /**
- * Sets the owner for the input ClippingPolygonCollection if there wasn't another owner.
- * Destroys the owner's previous ClippingPolygonCollection if setting is successful.
- *
- * @param {ClippingPolygonCollection} [clippingPolygonsCollection] A ClippingPolygonCollection (or undefined) being attached to an object
- * @param {object} owner An Object that should receive the new ClippingPolygonCollection
- * @param {string} key The Key for the Object to reference the ClippingPolygonCollection
- * @private
- */
- ClippingPolygonCollection.setOwner = function (
- clippingPolygonsCollection,
- owner,
- key,
- ) {
- // Don't destroy the ClippingPolygonCollection if it is already owned by newOwner
- if (clippingPolygonsCollection === owner[key]) {
- return;
- }
- // Destroy the existing ClippingPolygonCollection, if any
- owner[key] = owner[key] && owner[key].destroy();
- if (defined(clippingPolygonsCollection)) {
- //>>includeStart('debug', pragmas.debug);
- if (defined(clippingPolygonsCollection._owner)) {
- throw new DeveloperError(
- "ClippingPolygonCollection should only be assigned to one object",
- );
- }
- //>>includeEnd('debug');
- clippingPolygonsCollection._owner = owner;
- owner[key] = clippingPolygonsCollection;
- }
- };
-
- /**
- * Function for checking if the context will allow clipping polygons, which require floating point textures.
- *
- * @param {Scene|object} scene The scene that will contain clipped objects and clipping textures.
- * @returns {boolean} <code>true</code> if the context supports clipping polygons.
- */
- ClippingPolygonCollection.isSupported = function (scene) {
- return scene?.context.webgl2;
- };
-
- /**
- * Function for getting packed texture resolution.
- * If the ClippingPolygonCollection hasn't been updated, returns the resolution that will be
- * allocated based on the provided needed pixels.
- *
- * @param {Texture} texture The texture to be packed.
- * @param {number} pixelsNeeded The number of pixels needed based on the current polygon count.
- * @param {Cartesian2} result A Cartesian2 for the result.
- * @returns {Cartesian2} The required resolution.
- * @private
- */
- ClippingPolygonCollection.getTextureResolution = function (
- texture,
- pixelsNeeded,
- result,
- ) {
- if (defined(texture)) {
- result.x = texture.width;
- result.y = texture.height;
- return result;
- }
-
- const maxSize = ContextLimits.maximumTextureSize;
- result.x = Math.min(pixelsNeeded, maxSize);
- result.y = Math.ceil(pixelsNeeded / result.x);
-
- // Allocate twice as much space as needed to avoid frequent texture reallocation.
- result.y *= 2;
-
- return result;
- };
-
- /**
- * Function for getting the clipping collection's signed distance texture resolution.
- * If the ClippingPolygonCollection hasn't been updated, returns the resolution that will be
- * allocated based on the current settings
- *
- * @param {ClippingPolygonCollection} clippingPolygonCollection The clipping polygon collection
- * @param {Cartesian2} result A Cartesian2 for the result.
- * @returns {Cartesian2} The required resolution.
- * @private
- */
- ClippingPolygonCollection.getClippingDistanceTextureResolution = function (
- clippingPolygonCollection,
- result,
- ) {
- const texture = clippingPolygonCollection.signedDistanceTexture;
- if (defined(texture)) {
- result.x = texture.width;
- result.y = texture.height;
- return result;
- }
-
- const quality = clippingPolygonCollection.quality;
- const baseSize = Math.max(128, Math.ceil(4096 * quality));
- result.x = Math.min(ContextLimits.maximumTextureSize, baseSize);
- result.y = Math.min(ContextLimits.maximumTextureSize, baseSize);
-
- return result;
- };
-
- /**
- * Function for getting the clipping collection's extents texture resolution.
- * If the ClippingPolygonCollection hasn't been updated, returns the resolution that will be
- * allocated based on the current polygon count.
- *
- * @param {ClippingPolygonCollection} clippingPolygonCollection The clipping polygon collection
- * @param {Cartesian2} result A Cartesian2 for the result.
- * @returns {Cartesian2} The required resolution.
- * @private
- */
- ClippingPolygonCollection.getClippingExtentsTextureResolution = function (
- clippingPolygonCollection,
- result,
- ) {
- const texture = clippingPolygonCollection.extentsTexture;
- if (defined(texture)) {
- result.x = texture.width;
- result.y = texture.height;
- return result;
- }
-
- return ClippingPolygonCollection.getTextureResolution(
- texture,
- clippingPolygonCollection.pixelsNeededForExtents,
- result,
- );
- };
-
- /**
- * Returns true if this object was destroyed; otherwise, false.
- * <br /><br />
- * If this object was destroyed, it should not be used; calling any function other than
- * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
- *
- * @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
- *
- * @see ClippingPolygonCollection#destroy
- */
- ClippingPolygonCollection.prototype.isDestroyed = function () {
- return false;
- };
-
- /**
- * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
- * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
- * <br /><br />
- * Once an object is destroyed, it should not be used; calling any function other than
- * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
- * assign the return value (<code>undefined</code>) to the object as done in the example.
- *
- * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
- *
- *
- * @example
- * clippingPolygons = clippingPolygons && clippingPolygons.destroy();
- *
- * @see ClippingPolygonCollection#isDestroyed
- */
- ClippingPolygonCollection.prototype.destroy = function () {
- if (defined(this._signedDistanceComputeCommand)) {
- this._signedDistanceComputeCommand.canceled = true;
- }
-
- this._polygonsTexture =
- this._polygonsTexture && this._polygonsTexture.destroy();
- this._extentsTexture = this._extentsTexture && this._extentsTexture.destroy();
- this._signedDistanceTexture =
- this._signedDistanceTexture && this._signedDistanceTexture.destroy();
- return destroyObject(this);
- };
-
- export default ClippingPolygonCollection;
|