diff --git a/Apps/Sandcastle/gallery/AEC Clipping.html b/Apps/Sandcastle/gallery/AEC Clipping.html new file mode 100644 index 000000000000..03ea18d52693 --- /dev/null +++ b/Apps/Sandcastle/gallery/AEC Clipping.html @@ -0,0 +1,147 @@ + + +
+ + + + + +Left click to add a vertex. | +
Right click to add the polygon to the clipping collection. | +
v_
to be consistent with Cesium's style guide.
+ * @param {string} [qualifier] A qualifier for the varying, such as flat
.
*
* @example
* // creates the line "in vec3 v_color;" in the vertex shader
* // creates the line "out vec3 v_color;" in the fragment shader
* shaderBuilder.addVarying("vec3", "v_color");
*/
-ShaderBuilder.prototype.addVarying = function (type, identifier) {
+ShaderBuilder.prototype.addVarying = function (type, identifier, qualifier) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("type", type);
Check.typeOf.string("identifier", identifier);
//>>includeEnd('debug');
+ qualifier = defined(qualifier) ? `${qualifier} ` : "";
+
const line = `${type} ${identifier};`;
- this._vertexShaderParts.varyingLines.push(`out ${line}`);
- this._fragmentShaderParts.varyingLines.push(`in ${line}`);
+ this._vertexShaderParts.varyingLines.push(`${qualifier}out ${line}`);
+ this._fragmentShaderParts.varyingLines.push(`${qualifier}in ${line}`);
};
/**
diff --git a/packages/engine/Source/Scene/Cesium3DTile.js b/packages/engine/Source/Scene/Cesium3DTile.js
index 8dd01a1f77a3..2d6a2d80974b 100644
--- a/packages/engine/Source/Scene/Cesium3DTile.js
+++ b/packages/engine/Source/Scene/Cesium3DTile.js
@@ -411,6 +411,16 @@ function Cesium3DTile(tileset, baseResource, header, parent) {
*/
this.clippingPlanesDirty = false;
+ /**
+ * Tracks if the tile's relationship with a ClippingPolygonCollection has changed with regards
+ * to the ClippingPolygonCollection's state.
+ *
+ * @type {boolean}
+ *
+ * @private
+ */
+ this.clippingPolygonsDirty = false;
+
/**
* Tracks if the tile's request should be deferred until all non-deferred
* tiles load.
@@ -481,7 +491,9 @@ function Cesium3DTile(tileset, baseResource, header, parent) {
this._refines = false;
this._shouldSelect = false;
this._isClipped = true;
+ this._isClippedByPolygon = false;
this._clippingPlanesState = 0; // encapsulates (_isClipped, clippingPlanes.enabled) and number/function
+ this._clippingPolygonsState = 0; // encapsulates (_isClipped, clippingPolygons.enabled) and number/function
this._debugBoundingVolume = undefined;
this._debugContentBoundingVolume = undefined;
this._debugViewerRequestVolume = undefined;
@@ -1418,6 +1430,8 @@ Cesium3DTile.prototype.unloadContent = function () {
this.lastStyleTime = 0.0;
this.clippingPlanesDirty = this._clippingPlanesState === 0;
this._clippingPlanesState = 0;
+ this.clippingPolygonsDirty = this._clippingPolygonsState === 0;
+ this._clippingPolygonsState = 0;
this._debugColorizeTiles = false;
@@ -1516,6 +1530,17 @@ Cesium3DTile.prototype.visibility = function (
}
}
+ const clippingPolygons = tileset.clippingPolygons;
+ if (defined(clippingPolygons) && clippingPolygons.enabled) {
+ const intersection = clippingPolygons.computeIntersectionWithBoundingVolume(
+ boundingVolume
+ );
+
+ this._isClippedByPolygon = intersection !== Intersect.OUTSIDE;
+ // Polygon clipping intersections are determined by outer rectangles, therefore we cannot
+ // preemptively determine if a tile is completely clipped or not here.
+ }
+
return cullingVolume.computeVisibilityWithPlaneMask(
boundingVolume,
parentVisibilityPlaneMask
@@ -1562,6 +1587,17 @@ Cesium3DTile.prototype.contentVisibility = function (frameState) {
}
}
+ const clippingPolygons = tileset.clippingPolygons;
+ if (defined(clippingPolygons) && clippingPolygons.enabled) {
+ const intersection = clippingPolygons.computeIntersectionWithBoundingVolume(
+ boundingVolume
+ );
+ this._isClippedByPolygon = intersection !== Intersect.OUTSIDE;
+ if (intersection === Intersect.INSIDE) {
+ return Intersect.OUTSIDE;
+ }
+ }
+
return cullingVolume.computeVisibility(boundingVolume);
};
@@ -2150,6 +2186,33 @@ function updateClippingPlanes(tile, tileset) {
}
}
+/**
+ * Compute and compare ClippingPolygons state:
+ * - enabled-ness - are clipping polygons enabled? is this tile clipped?
+ * - clipping polygon count & position count
+ * - clipping function (inverse)
+
+ * @private
+ * @param {Cesium3DTile} tile
+ * @param {Cesium3DTileset} tileset
+ */
+function updateClippingPolygons(tile, tileset) {
+ const clippingPolygons = tileset.clippingPolygons;
+ let currentClippingPolygonsState = 0;
+ if (
+ defined(clippingPolygons) &&
+ tile._isClippedByPolygon &&
+ clippingPolygons.enabled
+ ) {
+ currentClippingPolygonsState = clippingPolygons.clippingPolygonsState;
+ }
+ // If clippingPolygonState for tile changed, mark clippingPolygonsDirty so content can update
+ if (currentClippingPolygonsState !== tile._clippingPolygonsState) {
+ tile._clippingPolygonsState = currentClippingPolygonsState;
+ tile.clippingPolygonsDirty = true;
+ }
+}
+
/**
* Get the draw commands needed to render this tile.
*
@@ -2163,6 +2226,7 @@ Cesium3DTile.prototype.update = function (tileset, frameState, passOptions) {
const commandStart = commandList.length;
updateClippingPlanes(this, tileset);
+ updateClippingPolygons(this, tileset);
applyDebugSettings(this, tileset, frameState, passOptions);
updateContent(this, tileset, frameState);
@@ -2176,6 +2240,7 @@ Cesium3DTile.prototype.update = function (tileset, frameState, passOptions) {
}
this.clippingPlanesDirty = false; // reset after content update
+ this.clippingPolygonsDirty = false;
};
const scratchCommandList = [];
diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js
index b09711b63874..abcb2071b8b6 100644
--- a/packages/engine/Source/Scene/Cesium3DTileset.js
+++ b/packages/engine/Source/Scene/Cesium3DTileset.js
@@ -41,6 +41,7 @@ import Cesium3DTilesetHeatmap from "./Cesium3DTilesetHeatmap.js";
import Cesium3DTilesetStatistics from "./Cesium3DTilesetStatistics.js";
import Cesium3DTileStyleEngine from "./Cesium3DTileStyleEngine.js";
import ClippingPlaneCollection from "./ClippingPlaneCollection.js";
+import ClippingPolygonCollection from "./ClippingPolygonCollection.js";
import hasExtension from "./hasExtension.js";
import ImplicitTileset from "./ImplicitTileset.js";
import ImplicitTileCoordinates from "./ImplicitTileCoordinates.js";
@@ -97,6 +98,7 @@ import Ray from "../Core/Ray.js";
* @property {boolean} [immediatelyLoadDesiredLevelOfDetail=false] When skipLevelOfDetail
is true
, only tiles that meet the maximum screen space error will ever be downloaded. Skipping factors are ignored and just the desired tiles are loaded.
* @property {boolean} [loadSiblings=false] When skipLevelOfDetail
is true
, determines whether siblings of visible tiles are always downloaded during traversal.
* @property {ClippingPlaneCollection} [clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset.
+ * @property {ClippingPolygonCollection} [clippingPolygons] The {@link ClippingPolygonCollection} used to selectively disable rendering the tileset.
* @property {ClassificationType} [classificationType] Determines whether terrain, 3D Tiles or both will be classified by this tileset. See {@link Cesium3DTileset#classificationType} for details about restrictions and limitations.
* @property {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid determining the size and shape of the globe.
* @property {object} [pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point attenuation based on geometric error and lighting.
@@ -800,7 +802,22 @@ function Cesium3DTileset(options) {
this.loadSiblings = defaultValue(options.loadSiblings, false);
this._clippingPlanes = undefined;
- this.clippingPlanes = options.clippingPlanes;
+ if (defined(options.clippingPlanes)) {
+ ClippingPlaneCollection.setOwner(
+ options.clippingPlanes,
+ this,
+ "_clippingPlanes"
+ );
+ }
+
+ this._clippingPolygons = undefined;
+ if (defined(options.clippingPolygons)) {
+ ClippingPolygonCollection.setOwner(
+ options.clippingPolygons,
+ this,
+ "_clippingPolygons"
+ );
+ }
if (defined(options.imageBasedLighting)) {
this._imageBasedLighting = options.imageBasedLighting;
@@ -1120,6 +1137,22 @@ Object.defineProperties(Cesium3DTileset.prototype, {
},
},
+ /**
+ * The {@link ClippingPolygonCollection} used to selectively disable rendering the tileset.
+ *
+ * @memberof Cesium3DTileset.prototype
+ *
+ * @type {ClippingPolygonCollection}
+ */
+ clippingPolygons: {
+ get: function () {
+ return this._clippingPolygons;
+ },
+ set: function (value) {
+ ClippingPolygonCollection.setOwner(value, this, "_clippingPolygons");
+ },
+ },
+
/**
* Gets the tileset's properties dictionary object, which contains metadata about per-feature properties.
*
@@ -2519,6 +2552,12 @@ Cesium3DTileset.prototype.prePassesUpdate = function (frameState) {
clippingPlanes.update(frameState);
}
+ // Update clipping polygons
+ const clippingPolygons = this._clippingPolygons;
+ if (defined(clippingPolygons) && clippingPolygons.enabled) {
+ clippingPolygons.update(frameState);
+ }
+
if (!defined(this._loadTimestamp)) {
this._loadTimestamp = JulianDate.clone(frameState.time);
}
@@ -3372,6 +3411,12 @@ Cesium3DTileset.prototype.updateForPass = function (
originalCullingVolume
);
+ // Update clipping polygons
+ const clippingPolygons = this._clippingPolygons;
+ if (defined(clippingPolygons) && clippingPolygons.enabled) {
+ clippingPolygons.queueCommands(frameState);
+ }
+
const passStatistics = this._statisticsPerPass[pass];
if (this.show || ignoreCommands) {
@@ -3440,6 +3485,8 @@ Cesium3DTileset.prototype.destroy = function () {
this._tileDebugLabels =
this._tileDebugLabels && this._tileDebugLabels.destroy();
this._clippingPlanes = this._clippingPlanes && this._clippingPlanes.destroy();
+ this._clippingPolygons =
+ this._clippingPolygons && this._clippingPolygons.destroy();
// Traverse the tree and destroy all tiles
if (defined(this._root)) {
diff --git a/packages/engine/Source/Scene/ClippingPolygon.js b/packages/engine/Source/Scene/ClippingPolygon.js
new file mode 100644
index 000000000000..786a63f9981b
--- /dev/null
+++ b/packages/engine/Source/Scene/ClippingPolygon.js
@@ -0,0 +1,212 @@
+import Check from "../Core/Check.js";
+import Cartesian3 from "../Core/Cartesian3.js";
+import Cartographic from "../Core/Cartographic.js";
+import defaultValue from "../Core/defaultValue.js";
+import defined from "../Core/defined.js";
+import Ellipsoid from "../Core/Ellipsoid.js";
+import CesiumMath from "../Core/Math.js";
+import PolygonGeometry from "../Core/PolygonGeometry.js";
+import Rectangle from "../Core/Rectangle.js";
+
+/**
+ * A geodesic polygon to be used with {@link ClippingPlaneCollection} for selectively hiding regions in a model, a 3D tileset, or the globe.
+ * @alias ClippingPolygon
+ * @constructor
+ *
+ * @param {object} options Object with the following properties:
+ * @param {Cartesian3[]} options.positions A list of three or more Cartesian coordinates defining the outer ring of the clipping polygon.
+ * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84]
+ *
+ * @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
+ * });
+ */
+function ClippingPolygon(options) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("options", options);
+ Check.typeOf.object("options.positions", options.positions);
+ Check.typeOf.number.greaterThanOrEquals(
+ "options.positions.length",
+ options.positions.length,
+ 3
+ );
+ //>>includeEnd('debug');
+
+ this._ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84);
+ this._positions = [...options.positions];
+}
+
+Object.defineProperties(ClippingPolygon.prototype, {
+ /**
+ * Returns the total number of positions in the polygon, include any holes.
+ *
+ * @memberof ClippingPolygon.prototype
+ * @type {number}
+ * @readonly
+ */
+ length: {
+ get: function () {
+ return this._positions.length;
+ },
+ },
+ /**
+ * Returns the outer ring of positions.
+ *
+ * @memberof ClippingPolygon.prototype
+ * @type {Cartesian3[]}
+ * @readonly
+ */
+ positions: {
+ get: function () {
+ return this._positions;
+ },
+ },
+ /**
+ * Returns the ellipsoid used to project the polygon onto surfaces when clipping.
+ *
+ * @memberof ClippingPolygon.prototype
+ * @type {Ellipsoid}
+ * @readonly
+ */
+ ellipsoid: {
+ get: function () {
+ return this._ellipsoid;
+ },
+ },
+});
+
+/**
+ * Clones the ClippingPolygon without setting its ownership.
+ * @param {ClippingPolygon} polygon The ClippingPolygon to be cloned
+ * @param {ClippingPolygon} [result] The object on which to store the cloned parameters.
+ * @returns {ClippingPolygon} a clone of the input ClippingPolygon
+ */
+ClippingPolygon.clone = function (polygon, result) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("polygon", polygon);
+ //>>includeEnd('debug');
+
+ if (!defined(result)) {
+ return new ClippingPolygon({
+ positions: polygon.positions,
+ ellipsoid: polygon.ellipsoid,
+ });
+ }
+
+ result._ellipsoid = polygon.ellipsoid;
+ result._positions.length = 0;
+ result._positions.push(...polygon.positions);
+ return result;
+};
+
+/**
+ * Compares the provided ClippingPolygons and returns
+ * true
if they are equal, false
otherwise.
+ *
+ * @param {Plane} left The first polygon.
+ * @param {Plane} right The second polygon.
+ * @returns {boolean} true
if left and right are equal, false
otherwise.
+ */
+ClippingPolygon.equals = function (left, right) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("left", left);
+ Check.typeOf.object("right", right);
+ //>>includeEnd('debug');
+
+ return (
+ left.ellipsoid.equals(right.ellipsoid) && left.positions === right.positions
+ );
+};
+
+/**
+ * Computes a cartographic rectangle which encloses the polygon defined by the list of positions, including cases over the international date line and the poles.
+ *
+ * @param {Rectangle} [result] An object in which to store the result.
+ * @returns {Rectangle} The result rectangle
+ */
+ClippingPolygon.prototype.computeRectangle = function (result) {
+ return PolygonGeometry.computeRectangleFromPositions(
+ this.positions,
+ this.ellipsoid,
+ undefined,
+ result
+ );
+};
+
+const scratchRectangle = new Rectangle();
+const spherePointScratch = new Cartesian3();
+/**
+ * Computes a rectangle with the spherical extents that encloses the polygon defined by the list of positions, including cases over the international date line and the poles.
+ *
+ * @private
+ *
+ * @param {Rectangle} [result] An object in which to store the result.
+ * @returns {Rectangle} The result rectangle with spherical extents.
+ */
+ClippingPolygon.prototype.computeSphericalExtents = function (result) {
+ if (!defined(result)) {
+ result = new Rectangle();
+ }
+
+ const rectangle = this.computeRectangle(scratchRectangle);
+
+ let spherePoint = Cartographic.toCartesian(
+ Rectangle.southwest(rectangle),
+ this.ellipsoid,
+ spherePointScratch
+ );
+
+ // Project into plane with vertical for latitude
+ let magXY = Math.sqrt(
+ spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y
+ );
+
+ // Use fastApproximateAtan2 for alignment with shader
+ let sphereLatitude = CesiumMath.fastApproximateAtan2(magXY, spherePoint.z);
+ let sphereLongitude = CesiumMath.fastApproximateAtan2(
+ spherePoint.x,
+ spherePoint.y
+ );
+
+ result.south = sphereLatitude;
+ result.west = sphereLongitude;
+
+ spherePoint = Cartographic.toCartesian(
+ Rectangle.northeast(rectangle),
+ this.ellipsoid,
+ spherePointScratch
+ );
+
+ // Project into plane with vertical for latitude
+ magXY = Math.sqrt(
+ spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y
+ );
+
+ // Use fastApproximateAtan2 for alignment with shader
+ sphereLatitude = CesiumMath.fastApproximateAtan2(magXY, spherePoint.z);
+ sphereLongitude = CesiumMath.fastApproximateAtan2(
+ spherePoint.x,
+ spherePoint.y
+ );
+
+ result.north = sphereLatitude;
+ result.east = sphereLongitude;
+
+ return result;
+};
+
+export default ClippingPolygon;
diff --git a/packages/engine/Source/Scene/ClippingPolygonCollection.js b/packages/engine/Source/Scene/ClippingPolygonCollection.js
new file mode 100644
index 000000000000..6fb039f71200
--- /dev/null
+++ b/packages/engine/Source/Scene/ClippingPolygonCollection.js
@@ -0,0 +1,968 @@
+import Cartesian2 from "../Core/Cartesian2.js";
+import CesiumMath from "../Core/Math.js";
+import Check from "../Core/Check.js";
+import defaultValue from "../Core/defaultValue.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";
+
+/**
+ * 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.
+ *
+ * @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 = defaultValue(options, defaultValue.EMPTY_OBJECT);
+
+ this._polygons = [];
+ this._totalPositions = 0;
+
+ /**
+ * If true, clipping will be enabled.
+ *
+ * @memberof ClippingPolygonCollection.prototype
+ * @type {boolean}
+ * @default true
+ */
+ this.enabled = defaultValue(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.
+ *
+ * @memberof ClippingPolygonCollection.prototype
+ * @type {boolean}
+ * @default false
+ */
+ this.inverse = defaultValue(options.inverse, false);
+
+ /**
+ * 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 is the number of positions of that polygon, followed by the list of positions
+ return this.totalPositions + 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} true
if the polygon was removed; false
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;
+};
+
+const scratchRectangle = new Rectangle();
+
+// Map the polygons to a list of extents-- Overlapping extents will be merged
+// into a single encompassing extent
+function getExtents(polygons) {
+ const extentsList = [];
+ const polygonIndicesList = [];
+
+ const length = polygons.length;
+ for (let polygonIndex = 0; polygonIndex < length; ++polygonIndex) {
+ const polygon = polygons[polygonIndex];
+ const extents = polygon.computeSphericalExtents();
+
+ let height = Math.max(extents.height * 2.5, 0.001);
+ let width = Math.max(extents.width * 2.5, 0.001);
+
+ // Pad extents to avoid floating point error when fragment culling at edges.
+ let paddedExtents = Rectangle.clone(extents);
+ paddedExtents.south -= height;
+ paddedExtents.west -= width;
+ paddedExtents.north += height;
+ paddedExtents.east += width;
+
+ 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);
+
+ const polygonIndices = [polygonIndex];
+ for (let i = 0; i < extentsList.length; ++i) {
+ const e = extentsList[i];
+ if (
+ defined(e) &&
+ defined(Rectangle.simpleIntersection(e, paddedExtents)) &&
+ !Rectangle.equals(e, paddedExtents)
+ ) {
+ const intersectingPolygons = polygonIndicesList[i];
+ polygonIndices.push(...intersectingPolygons);
+ intersectingPolygons.reduce(
+ (extents, p) =>
+ Rectangle.union(
+ polygons[p].computeSphericalExtents(scratchRectangle),
+ extents,
+ extents
+ ),
+ extents
+ );
+
+ extentsList[i] = undefined;
+ polygonIndicesList[i] = undefined;
+
+ height = Math.max(extents.height * 2.5, 0.001);
+ width = Math.max(extents.width * 2.5, 0.001);
+
+ paddedExtents = Rectangle.clone(extents, paddedExtents);
+ paddedExtents.south -= height;
+ paddedExtents.west -= width;
+ paddedExtents.north += height;
+ paddedExtents.east += width;
+
+ 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);
+
+ // Reiterate through the extents list until there are no more intersections
+ i = -1;
+ }
+ }
+
+ extentsList.push(paddedExtents);
+ polygonIndicesList.push(polygonIndices);
+ }
+
+ const extentsIndexByPolygon = new Map();
+ polygonIndicesList
+ .filter(defined)
+ .forEach((polygonIndices, e) =>
+ polygonIndices.forEach((p) => extentsIndexByPolygon.set(p, e))
+ );
+
+ return {
+ extentsList: extentsList.filter(defined),
+ extentsIndexByPolygon: 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;
+
+ const { extentsList, extentsIndexByPolygon } = getExtents(polygons);
+
+ let floatIndex = 0;
+ for (const [polygonIndex, polygon] of polygons.entries()) {
+ // 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 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;
+ }
+ }
+
+ // Pack extents
+ 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.
+ *
+ * Do not call this function directly. + *
+ * @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." + ); + } + + // 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); +}; + +/** + * Called when {@link Viewer} or {@link CesiumWidget} render the scene to + * build the resources for clipping polygons. + *+ * Do not call this function directly. + *
+ * @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(); +/** + * 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=WGS84] 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; + } + + for (let i = 0; i < length; ++i) { + const polygon = polygons[i]; + + const polygonBoundingRectangle = polygon.computeRectangle(); + 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 + ); + } + + const result = Rectangle.simpleIntersection( + tileBoundingRectangle, + polygonBoundingRectangle, + scratchRectangleIntersection + ); + + if (defined(result)) { + intersection = 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}true
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;
+ }
+
+ result.x = Math.min(ContextLimits.maximumTextureSize, 4096);
+ result.y = Math.min(ContextLimits.maximumTextureSize, 4096);
+
+ 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.
+ * isDestroyed
will result in a {@link DeveloperError} exception.
+ *
+ * @returns {boolean} true
if this object was destroyed; otherwise, false
.
+ *
+ * @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.
+ * isDestroyed
will result in a {@link DeveloperError} exception. Therefore,
+ * assign the return value (undefined
) 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;
diff --git a/packages/engine/Source/Scene/Globe.js b/packages/engine/Source/Scene/Globe.js
index 07d2b83d6316..1a406902e0af 100644
--- a/packages/engine/Source/Scene/Globe.js
+++ b/packages/engine/Source/Scene/Globe.js
@@ -449,6 +449,20 @@ Object.defineProperties(Globe.prototype, {
this._surface.tileProvider.clippingPlanes = value;
},
},
+ /**
+ * A property specifying a {@link ClippingPolygonCollection} used to selectively disable rendering inside or outside a list of polygons.
+ *
+ * @memberof Globe.prototype
+ * @type {ClippingPolygonCollection}
+ */
+ clippingPolygons: {
+ get: function () {
+ return this._surface.tileProvider.clippingPolygons;
+ },
+ set: function (value) {
+ this._surface.tileProvider.clippingPolygons = value;
+ },
+ },
/**
* A property specifying a {@link Rectangle} used to limit globe rendering to a cartographic area.
* Defaults to the maximum extent of cartographic coordinates.
diff --git a/packages/engine/Source/Scene/GlobeSurfaceShaderSet.js b/packages/engine/Source/Scene/GlobeSurfaceShaderSet.js
index 813f8296131b..a7a48d624b87 100644
--- a/packages/engine/Source/Scene/GlobeSurfaceShaderSet.js
+++ b/packages/engine/Source/Scene/GlobeSurfaceShaderSet.js
@@ -10,13 +10,15 @@ function GlobeSurfaceShader(
flags,
material,
shaderProgram,
- clippingShaderState
+ clippingShaderState,
+ clippingPolygonShaderState
) {
this.numberOfDayTextures = numberOfDayTextures;
this.flags = flags;
this.material = material;
this.shaderProgram = shaderProgram;
this.clippingShaderState = clippingShaderState;
+ this.clippingPolygonShaderState = clippingPolygonShaderState;
}
/**
@@ -60,6 +62,31 @@ function getPositionMode(sceneMode) {
return positionMode;
}
+function getPolygonClippingFunction(context) {
+ // return a noop for webgl1
+ if (!context.webgl2) {
+ return `void clipPolygons(highp sampler2D clippingDistance, int regionsLength, vec2 clippingPosition, int regionIndex) {
+ }`;
+ }
+
+ return `void clipPolygons(highp sampler2D clippingDistance, int regionsLength, vec2 clippingPosition, int regionIndex) {
+ czm_clipPolygons(clippingDistance, regionsLength, clippingPosition, regionIndex);
+ }`;
+}
+
+function getUnpackClippingFunction(context) {
+ // return a noop for webgl1
+ if (!context.webgl2) {
+ return `vec4 unpackClippingExtents(highp sampler2D extentsTexture, int index) {
+ return vec4();
+ }`;
+ }
+
+ return `vec4 unpackClippingExtents(highp sampler2D extentsTexture, int index) {
+ return czm_unpackClippingExtents(extentsTexture, index);
+ }`;
+}
+
function get2DYPositionFraction(useWebMercatorProjection) {
const get2DYPositionFractionGeographicProjection =
"float get2DYPositionFraction(vec2 textureCoordinates) { return get2DGeographicYPositionFraction(textureCoordinates); }";
@@ -95,6 +122,8 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) {
const enableFog = options.enableFog;
const enableClippingPlanes = options.enableClippingPlanes;
const clippingPlanes = options.clippingPlanes;
+ const enableClippingPolygons = options.enableClippingPolygons;
+ const clippingPolygons = options.clippingPolygons;
const clippedByBoundaries = options.clippedByBoundaries;
const hasImageryLayerCutout = options.hasImageryLayerCutout;
const colorCorrect = options.colorCorrect;
@@ -152,16 +181,17 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) {
(quantization << 18) |
(applySplit << 19) |
(enableClippingPlanes << 20) |
- (cartographicLimitRectangleFlag << 21) |
- (imageryCutoutFlag << 22) |
- (colorCorrect << 23) |
- (highlightFillTile << 24) |
- (colorToAlpha << 25) |
- (hasGeodeticSurfaceNormals << 26) |
- (hasExaggeration << 27) |
- (showUndergroundColor << 28) |
- (translucent << 29) |
- (applyDayNightAlpha << 30);
+ (enableClippingPolygons << 21) |
+ (cartographicLimitRectangleFlag << 22) |
+ (imageryCutoutFlag << 23) |
+ (colorCorrect << 24) |
+ (highlightFillTile << 25) |
+ (colorToAlpha << 26) |
+ (hasGeodeticSurfaceNormals << 27) |
+ (hasExaggeration << 28) |
+ (showUndergroundColor << 29) |
+ (translucent << 30) |
+ (applyDayNightAlpha << 31);
let currentClippingShaderState = 0;
if (defined(clippingPlanes) && clippingPlanes.length > 0) {
@@ -169,13 +199,23 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) {
? clippingPlanes.clippingPlanesState
: 0;
}
+
+ let currentClippingPolygonsShaderState = 0;
+ if (defined(clippingPolygons) && clippingPolygons.length > 0) {
+ currentClippingPolygonsShaderState = enableClippingPolygons
+ ? clippingPolygons.clippingPolygonsState
+ : 0;
+ }
+
let surfaceShader = surfaceTile.surfaceShader;
if (
defined(surfaceShader) &&
surfaceShader.numberOfDayTextures === numberOfDayTextures &&
surfaceShader.flags === flags &&
surfaceShader.material === this.material &&
- surfaceShader.clippingShaderState === currentClippingShaderState
+ surfaceShader.clippingShaderState === currentClippingShaderState &&
+ surfaceShader.clippingPolygonShaderState ===
+ currentClippingPolygonsShaderState
) {
return surfaceShader.shaderProgram;
}
@@ -190,16 +230,25 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) {
if (
!defined(surfaceShader) ||
surfaceShader.material !== this.material ||
- surfaceShader.clippingShaderState !== currentClippingShaderState
+ surfaceShader.clippingShaderState !== currentClippingShaderState ||
+ surfaceShader.clippingPolygonShaderState !==
+ currentClippingPolygonsShaderState
) {
// Cache miss - we've never seen this combination of numberOfDayTextures and flags before.
const vs = this.baseVertexShaderSource.clone();
const fs = this.baseFragmentShaderSource.clone();
+ // Need to go before GlobeFS
if (currentClippingShaderState !== 0) {
fs.sources.unshift(
getClippingFunction(clippingPlanes, frameState.context)
- ); // Need to go before GlobeFS
+ );
+ }
+
+ // Need to go before GlobeFS
+ if (currentClippingPolygonsShaderState !== 0) {
+ fs.sources.unshift(getPolygonClippingFunction(frameState.context));
+ vs.sources.unshift(getUnpackClippingFunction(frameState.context));
}
vs.defines.push(quantizationDefine);
@@ -292,6 +341,22 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) {
fs.defines.push("ENABLE_CLIPPING_PLANES");
}
+ if (enableClippingPolygons) {
+ fs.defines.push("ENABLE_CLIPPING_POLYGONS");
+ vs.defines.push("ENABLE_CLIPPING_POLYGONS");
+
+ if (clippingPolygons.inverse) {
+ fs.defines.push("CLIPPING_INVERSE");
+ }
+
+ fs.defines.push(
+ `CLIPPING_POLYGON_REGIONS_LENGTH ${clippingPolygons.extentsCount}`
+ );
+ vs.defines.push(
+ `CLIPPING_POLYGON_REGIONS_LENGTH ${clippingPolygons.extentsCount}`
+ );
+ }
+
if (colorCorrect) {
fs.defines.push("COLOR_CORRECT");
}
@@ -377,7 +442,8 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) {
flags,
this.material,
shader,
- currentClippingShaderState
+ currentClippingShaderState,
+ currentClippingPolygonsShaderState
);
}
diff --git a/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js b/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js
index a6851b02e469..ab4ec1e9a9e7 100644
--- a/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js
+++ b/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js
@@ -38,6 +38,7 @@ import RenderState from "../Renderer/RenderState.js";
import VertexArray from "../Renderer/VertexArray.js";
import BlendingState from "./BlendingState.js";
import ClippingPlaneCollection from "./ClippingPlaneCollection.js";
+import ClippingPolygonCollection from "./ClippingPolygonCollection.js";
import DepthFunction from "./DepthFunction.js";
import GlobeSurfaceTile from "./GlobeSurfaceTile.js";
import ImageryLayer from "./ImageryLayer.js";
@@ -170,6 +171,13 @@ function GlobeSurfaceTileProvider(options) {
*/
this._clippingPlanes = undefined;
+ /**
+ * A property specifying a {@link ClippingPolygonCollection} used to selectively disable rendering inside or outside a list of polygons.
+ * @type {ClippingPolygonCollection}
+ * @private
+ */
+ this._clippingPolygons = undefined;
+
/**
* A property specifying a {@link Rectangle} used to selectively limit terrain and imagery rendering.
* @type {Rectangle}
@@ -290,7 +298,7 @@ Object.defineProperties(GlobeSurfaceTileProvider.prototype, {
},
},
/**
- * The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset.
+ * The {@link ClippingPlaneCollection} used to selectively disable rendering.
*
* @type {ClippingPlaneCollection}
*
@@ -304,6 +312,22 @@ Object.defineProperties(GlobeSurfaceTileProvider.prototype, {
ClippingPlaneCollection.setOwner(value, this, "_clippingPlanes");
},
},
+
+ /**
+ * The {@link ClippingPolygonCollection} used to selectively disable rendering inside or outside a list of polygons.
+ *
+ * @type {ClippingPolygonCollection}
+ *
+ * @private
+ */
+ clippingPolygons: {
+ get: function () {
+ return this._clippingPolygons;
+ },
+ set: function (value) {
+ ClippingPolygonCollection.setOwner(value, this, "_clippingPolygons");
+ },
+ },
});
function sortTileImageryByLayerIndex(a, b) {
@@ -391,6 +415,14 @@ GlobeSurfaceTileProvider.prototype.beginUpdate = function (frameState) {
if (defined(clippingPlanes) && clippingPlanes.enabled) {
clippingPlanes.update(frameState);
}
+
+ // update clipping polygons
+ const clippingPolygons = this._clippingPolygons;
+ if (defined(clippingPolygons) && clippingPolygons.enabled) {
+ clippingPolygons.update(frameState);
+ clippingPolygons.queueCommands(frameState);
+ }
+
this._usedDrawCommands = 0;
this._hasLoadedTilesThisFrame = false;
@@ -657,6 +689,11 @@ function isUndergroundVisible(tileProvider, frameState) {
return true;
}
+ const clippingPolygons = tileProvider._clippingPolygons;
+ if (defined(clippingPolygons) && clippingPolygons.enabled) {
+ return true;
+ }
+
if (
!Rectangle.equals(
tileProvider.cartographicLimitRectangle,
@@ -775,6 +812,16 @@ GlobeSurfaceTileProvider.prototype.computeTileVisibility = function (
}
}
+ const clippingPolygons = this._clippingPolygons;
+ if (defined(clippingPolygons) && clippingPolygons.enabled) {
+ const polygonIntersection = clippingPolygons.computeIntersectionWithBoundingVolume(
+ tileBoundingRegion
+ );
+ tile.isClipped = polygonIntersection !== Intersect.OUTSIDE;
+ // Polygon clipping intersections are determined by outer rectangles, therefore we cannot
+ // preemptively determine if a tile is completely clipped or not here.
+ }
+
let visibility;
const intersection = cullingVolume.computeVisibility(boundingVolume);
@@ -1364,6 +1411,8 @@ GlobeSurfaceTileProvider.prototype.isDestroyed = function () {
GlobeSurfaceTileProvider.prototype.destroy = function () {
this._tileProvider = this._tileProvider && this._tileProvider.destroy();
this._clippingPlanes = this._clippingPlanes && this._clippingPlanes.destroy();
+ this._clippingPolygons =
+ this._clippingPolygons && this._clippingPolygons.destroy();
this._removeLayerAddedListener =
this._removeLayerAddedListener && this._removeLayerAddedListener();
this._removeLayerRemovedListener =
@@ -1755,6 +1804,21 @@ function createTileUniformMap(frameState, globeSurfaceTileProvider) {
style.alpha = this.properties.clippingPlanesEdgeWidth;
return style;
},
+ u_clippingDistance: function () {
+ const texture =
+ globeSurfaceTileProvider._clippingPolygons.clippingTexture;
+ if (defined(texture)) {
+ return texture;
+ }
+ return frameState.context.defaultTexture;
+ },
+ u_clippingExtents: function () {
+ const texture = globeSurfaceTileProvider._clippingPolygons.extentsTexture;
+ if (defined(texture)) {
+ return texture;
+ }
+ return frameState.context.defaultTexture;
+ },
u_minimumBrightness: function () {
return frameState.fog.minimumBrightness;
},
@@ -2039,6 +2103,8 @@ const surfaceShaderSetOptionsScratch = {
enableFog: undefined,
enableClippingPlanes: undefined,
clippingPlanes: undefined,
+ enableClippingPolygons: undefined,
+ clippingPolygons: undefined,
clippedByBoundaries: undefined,
hasImageryLayerCutout: undefined,
colorCorrect: undefined,
@@ -2166,6 +2232,13 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) {
) {
--maxTextures;
}
+ if (
+ defined(tileProvider.clippingPolygons) &&
+ tileProvider.clippingPolygons.enabled
+ ) {
+ --maxTextures;
+ --maxTextures;
+ }
maxTextures -= globeTranslucencyState.numberOfTextureUniforms;
@@ -2726,6 +2799,11 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) {
uniformMapProperties.clippingPlanesEdgeWidth = clippingPlanes.edgeWidth;
}
+ // update clipping polygons
+ const clippingPolygons = tileProvider._clippingPolygons;
+ const clippingPolygonsEnabled =
+ defined(clippingPolygons) && clippingPolygons.enabled && tile.isClipped;
+
surfaceShaderSetOptions.numberOfDayTextures = numberOfDayTextures;
surfaceShaderSetOptions.applyBrightness = applyBrightness;
surfaceShaderSetOptions.applyContrast = applyContrast;
@@ -2738,6 +2816,8 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) {
surfaceShaderSetOptions.enableFog = applyFog;
surfaceShaderSetOptions.enableClippingPlanes = clippingPlanesEnabled;
surfaceShaderSetOptions.clippingPlanes = clippingPlanes;
+ surfaceShaderSetOptions.enableClippingPolygons = clippingPolygonsEnabled;
+ surfaceShaderSetOptions.clippingPolygons = clippingPolygons;
surfaceShaderSetOptions.hasImageryLayerCutout = applyCutout;
surfaceShaderSetOptions.colorCorrect = colorCorrect;
surfaceShaderSetOptions.highlightFillTile = highlightFillTile;
diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js
index 548e25f3478c..5cc4449ef316 100644
--- a/packages/engine/Source/Scene/Model/Model.js
+++ b/packages/engine/Source/Scene/Model/Model.js
@@ -17,6 +17,7 @@ import Resource from "../../Core/Resource.js";
import RuntimeError from "../../Core/RuntimeError.js";
import Pass from "../../Renderer/Pass.js";
import ClippingPlaneCollection from "../ClippingPlaneCollection.js";
+import ClippingPolygonCollection from "../ClippingPolygonCollection.js";
import ColorBlendMode from "../ColorBlendMode.js";
import GltfLoader from "../GltfLoader.js";
import HeightReference, {
@@ -147,6 +148,7 @@ import pickModel from "./pickModel.js";
* @privateParam {boolean} [options.showOutline=true] Whether to display the outline for models using the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. When true, outlines are displayed. When false, outlines are not displayed.
* @privateParam {Color} [options.outlineColor=Color.BLACK] The color to use when rendering outlines.
* @privateParam {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the model.
+ * @privateParam {ClippingPolygonCollection} [options.clippingPolygons] The {@link ClippingPolygonCollection} used to selectively disable rendering the model.
* @privateParam {Cartesian3} [options.lightColor] The light color when shading the model. When undefined
the scene's light color is used instead.
* @privateParam {ImageBasedLighting} [options.imageBasedLighting] The properties for managing image-based lighting on this model.
* @privateParam {boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the material's doubleSided property; when false, back face culling is disabled. Back faces are not culled if the model's color is translucent.
@@ -370,6 +372,20 @@ function Model(options) {
this._clippingPlanesState = 0; // If this value changes, the shaders need to be regenerated.
this._clippingPlanesMatrix = Matrix4.clone(Matrix4.IDENTITY); // Derived from reference matrix and the current view matrix
+ // If the given clipping polygons don't have an owner, make this model its owner.
+ // Otherwise, the clipping polygons are passed down from a tileset.
+ const clippingPolygons = options.clippingPolygons;
+ if (defined(clippingPolygons) && clippingPolygons.owner === undefined) {
+ ClippingPolygonCollection.setOwner(
+ clippingPolygons,
+ this,
+ "_clippingPolygons"
+ );
+ } else {
+ this._clippingPolygons = clippingPolygons;
+ }
+ this._clippingPolygonsState = 0; // If this value changes, the shaders need to be regenerated.
+
this._lightColor = Cartesian3.clone(options.lightColor);
this._imageBasedLighting = defined(options.imageBasedLighting)
@@ -1303,6 +1319,26 @@ Object.defineProperties(Model.prototype, {
},
},
+ /**
+ * The {@link ClippingPolygonCollection} used to selectively disable rendering the model.
+ *
+ * @memberof Model.prototype
+ *
+ * @type {ClippingPolygonCollection}
+ */
+ clippingPolygons: {
+ get: function () {
+ return this._clippingPolygons;
+ },
+ set: function (value) {
+ if (value !== this._clippingPolygons) {
+ // Handle destroying old clipping polygons, new clipping polygons ownership
+ ClippingPolygonCollection.setOwner(value, this, "_clippingPolygons");
+ this.resetDrawCommands();
+ }
+ },
+ },
+
/**
* The light color when shading the model. When undefined
the scene's light color is used instead.
*
@@ -1801,6 +1837,7 @@ Model.prototype.update = function (frameState) {
updateSilhouette(this, frameState);
updateSkipLevelOfDetail(this, frameState);
updateClippingPlanes(this, frameState);
+ updateClippingPolygons(this, frameState);
updateSceneMode(this, frameState);
updateFog(this, frameState);
updateVerticalExaggeration(this, frameState);
@@ -1987,6 +2024,24 @@ function updateClippingPlanes(model, frameState) {
}
}
+function updateClippingPolygons(model, frameState) {
+ // Update the clipping polygon collection / state for this model to detect any changes.
+ let currentClippingPolygonsState = 0;
+ if (model.isClippingPolygonsEnabled()) {
+ if (model._clippingPolygons.owner === model) {
+ model._clippingPolygons.update(frameState);
+ model._clippingPolygons.queueCommands(frameState);
+ }
+ currentClippingPolygonsState =
+ model._clippingPolygons.clippingPolygonsState;
+ }
+
+ if (currentClippingPolygonsState !== model._clippingPolygonsState) {
+ model.resetDrawCommands();
+ model._clippingPolygonsState = currentClippingPolygonsState;
+ }
+}
+
function updateSceneMode(model, frameState) {
if (frameState.mode !== model._sceneMode) {
if (model._projectTo2D) {
@@ -2537,6 +2592,21 @@ Model.prototype.pick = function (
);
};
+/**
+ * Gets whether or not clipping polygons are enabled for this model.
+ *
+ * @returns {boolean} true
if clipping polygons are enabled for this model, false
.
+ * @private
+ */
+Model.prototype.isClippingPolygonsEnabled = function () {
+ const clippingPolygons = this._clippingPolygons;
+ return (
+ defined(clippingPolygons) &&
+ clippingPolygons.enabled &&
+ clippingPolygons.length !== 0
+ );
+};
+
/**
* Returns true if this object was destroyed; otherwise, false.
*
@@ -2606,6 +2676,17 @@ Model.prototype.destroy = function () {
}
this._clippingPlanes = undefined;
+ // Only destroy the ClippingPolygonCollection if this is the owner.
+ const clippingPolygonCollection = this._clippingPolygons;
+ if (
+ defined(clippingPolygonCollection) &&
+ !clippingPolygonCollection.isDestroyed() &&
+ clippingPolygonCollection.owner === this
+ ) {
+ clippingPolygonCollection.destroy();
+ }
+ this._clippingPolygons = undefined;
+
// Only destroy the ImageBasedLighting if this is the owner.
if (
this._shouldDestroyImageBasedLighting &&
@@ -2689,6 +2770,7 @@ Model.prototype.destroyModelResources = function () {
* @param {boolean} [options.showOutline=true] Whether to display the outline for models using the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. When true, outlines are displayed. When false, outlines are not displayed.
* @param {Color} [options.outlineColor=Color.BLACK] The color to use when rendering outlines.
* @param {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the model.
+ * @param {ClippingPolygonCollection} [options.clippingPolygons] The {@link ClippingPolygonCollection} used to selectively disable rendering the model.
* @param {Cartesian3} [options.lightColor] The light color when shading the model. When undefined
the scene's light color is used instead.
* @param {ImageBasedLighting} [options.imageBasedLighting] The properties for managing image-based lighting on this model.
* @param {boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the material's doubleSided property; when false, back face culling is disabled. Back faces are not culled if the model's color is translucent.
@@ -3051,6 +3133,7 @@ function makeModelOptions(loader, modelType, options) {
showOutline: options.showOutline,
outlineColor: options.outlineColor,
clippingPlanes: options.clippingPlanes,
+ clippingPolygons: options.clippingPolygons,
lightColor: options.lightColor,
imageBasedLighting: options.imageBasedLighting,
backFaceCulling: options.backFaceCulling,
diff --git a/packages/engine/Source/Scene/Model/Model3DTileContent.js b/packages/engine/Source/Scene/Model/Model3DTileContent.js
index 20707a03909f..a32206d3dba6 100644
--- a/packages/engine/Source/Scene/Model/Model3DTileContent.js
+++ b/packages/engine/Source/Scene/Model/Model3DTileContent.js
@@ -255,6 +255,27 @@ Model3DTileContent.prototype.update = function (tileset, frameState) {
model._clippingPlanesState = 0;
}
+ // Updating clipping polygons requires more effort because of ownership checks
+ const tilesetClippingPolygons = tileset.clippingPolygons;
+ if (defined(tilesetClippingPolygons) && tile.clippingPolygonsDirty) {
+ // Dereference the clipping polygons from the model if they are irrelevant.
+ model._clippingPolygons =
+ tilesetClippingPolygons.enabled && tile._isClippedByPolygon
+ ? tilesetClippingPolygons
+ : undefined;
+ }
+
+ // If the model references a different ClippingPolygonCollection from the tileset,
+ // update the model to use the new ClippingPolygonCollection.
+ if (
+ defined(tilesetClippingPolygons) &&
+ defined(model._clippingPolygons) &&
+ model._clippingPolygons !== tilesetClippingPolygons
+ ) {
+ model._clippingPolygons = tilesetClippingPolygons;
+ model._clippingPolygonsState = 0;
+ }
+
model.update(frameState);
if (!this._ready && model.ready) {
diff --git a/packages/engine/Source/Scene/Model/ModelClippingPolygonsPipelineStage.js b/packages/engine/Source/Scene/Model/ModelClippingPolygonsPipelineStage.js
new file mode 100644
index 000000000000..c9eb597140c7
--- /dev/null
+++ b/packages/engine/Source/Scene/Model/ModelClippingPolygonsPipelineStage.js
@@ -0,0 +1,92 @@
+import combine from "../../Core/combine.js";
+import ModelClippingPolygonsStageVS from "../../Shaders/Model/ModelClippingPolygonsStageVS.js";
+import ModelClippingPolygonsStageFS from "../../Shaders/Model/ModelClippingPolygonsStageFS.js";
+import ShaderDestination from "../../Renderer/ShaderDestination.js";
+
+/**
+ * The model clipping planes stage is responsible for applying clipping planes to the model.
+ *
+ * @namespace ModelClippingPolygonsPipelineStage
+ *
+ * @private
+ */
+const ModelClippingPolygonsPipelineStage = {
+ name: "ModelClippingPolygonsPipelineStage", // Helps with debugging
+};
+
+/**
+ * Process a model for polygon clipping. This modifies the following parts of the render resources:
+ *
+ *