智慧水务管理系统 - 精河县供水工程综合管理平台

ClippingPolygonCollection.js 38KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134
  1. import Cartesian2 from "../Core/Cartesian2.js";
  2. import CesiumMath from "../Core/Math.js";
  3. import Check from "../Core/Check.js";
  4. import Frozen from "../Core/Frozen.js";
  5. import defined from "../Core/defined.js";
  6. import destroyObject from "../Core/destroyObject.js";
  7. import DeveloperError from "../Core/DeveloperError.js";
  8. import Event from "../Core/Event.js";
  9. import Intersect from "../Core/Intersect.js";
  10. import PixelFormat from "../Core/PixelFormat.js";
  11. import Rectangle from "../Core/Rectangle.js";
  12. import ContextLimits from "../Renderer/ContextLimits.js";
  13. import PixelDatatype from "../Renderer/PixelDatatype.js";
  14. import RuntimeError from "../Core/RuntimeError.js";
  15. import Sampler from "../Renderer/Sampler.js";
  16. import Texture from "../Renderer/Texture.js";
  17. import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js";
  18. import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js";
  19. import TextureWrap from "../Renderer/TextureWrap.js";
  20. import ClippingPolygon from "./ClippingPolygon.js";
  21. import ComputeCommand from "../Renderer/ComputeCommand.js";
  22. import PolygonSignedDistanceFS from "../Shaders/PolygonSignedDistanceFS.js";
  23. import Pass from "../Renderer/Pass.js";
  24. /**
  25. * Specifies a set of clipping polygons. Clipping polygons selectively disable rendering in a region
  26. * inside or outside the specified list of {@link ClippingPolygon} objects for a single glTF model, 3D Tileset, or the globe.
  27. *
  28. * Clipping Polygons are only supported in WebGL 2 contexts.
  29. *
  30. * @alias ClippingPolygonCollection
  31. * @constructor
  32. *
  33. * @param {object} [options] Object with the following properties:
  34. * @param {ClippingPolygon[]} [options.polygons=[]] An array of {@link ClippingPolygon} objects used to selectively disable rendering on the inside of each polygon.
  35. * @param {boolean} [options.enabled=true] Determines whether the clipping polygons are active.
  36. * @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.
  37. * @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.
  38. *
  39. * @example
  40. * const positions = Cesium.Cartesian3.fromRadiansArray([
  41. * -1.3194369277314022,
  42. * 0.6988062530900625,
  43. * -1.31941,
  44. * 0.69879,
  45. * -1.3193955980204217,
  46. * 0.6988091578771254,
  47. * -1.3193931220959367,
  48. * 0.698743632490865,
  49. * -1.3194358224045408,
  50. * 0.6987471965556998,
  51. * ]);
  52. *
  53. * const polygon = new Cesium.ClippingPolygon({
  54. * positions: positions
  55. * });
  56. *
  57. * const polygons = new Cesium.ClippingPolygonCollection({
  58. * polygons: [ polygon ]
  59. * });
  60. */
  61. function ClippingPolygonCollection(options) {
  62. options = options ?? Frozen.EMPTY_OBJECT;
  63. this._polygons = [];
  64. this._totalPositions = 0;
  65. this.debugShowDistanceTexture = options.debugShowDistanceTexture ?? false;
  66. /**
  67. * If true, clipping will be enabled.
  68. *
  69. * @type {boolean}
  70. * @default true
  71. */
  72. this.enabled = options.enabled ?? true;
  73. /**
  74. * If true, a region will be clipped if it is outside of every polygon in the
  75. * collection. Otherwise, a region will only be clipped if it is
  76. * inside of any polygon.
  77. *
  78. * @type {boolean}
  79. * @default false
  80. */
  81. this.inverse = options.inverse ?? false;
  82. /**
  83. * A scalar that controls the resolution of the signed distance texture used for clipping.
  84. * Values greater than 1.0 increase quality, values less than 1.0 decrease it. Must be greater than 0.0.
  85. *
  86. * @type {number}
  87. * @default 1.0
  88. */
  89. this.quality = options.quality ?? 1.0;
  90. /**
  91. * An event triggered when a new clipping polygon is added to the collection. Event handlers
  92. * are passed the new polygon and the index at which it was added.
  93. * @type {Event}
  94. * @default Event()
  95. */
  96. this.polygonAdded = new Event();
  97. /**
  98. * An event triggered when a new clipping polygon is removed from the collection. Event handlers
  99. * are passed the new polygon and the index from which it was removed.
  100. * @type {Event}
  101. * @default Event()
  102. */
  103. this.polygonRemoved = new Event();
  104. // If this ClippingPolygonCollection has an owner, only its owner should update or destroy it.
  105. // This is because in a Cesium3DTileset multiple models may reference the tileset's ClippingPolygonCollection.
  106. this._owner = undefined;
  107. this._float32View = undefined;
  108. this._extentsFloat32View = undefined;
  109. this._extentsCount = 0;
  110. this._polygonsTexture = undefined;
  111. this._extentsTexture = undefined;
  112. this._signedDistanceTexture = undefined;
  113. this._signedDistanceComputeCommand = undefined;
  114. // Add each ClippingPolygon object.
  115. const polygons = options.polygons;
  116. if (defined(polygons)) {
  117. const polygonsLength = polygons.length;
  118. for (let i = 0; i < polygonsLength; ++i) {
  119. this._polygons.push(polygons[i]);
  120. }
  121. }
  122. }
  123. Object.defineProperties(ClippingPolygonCollection.prototype, {
  124. /**
  125. * Returns the number of polygons in this collection. This is commonly used with
  126. * {@link ClippingPolygonCollection#get} to iterate over all the polygons
  127. * in the collection.
  128. *
  129. * @memberof ClippingPolygonCollection.prototype
  130. * @type {number}
  131. * @readonly
  132. */
  133. length: {
  134. get: function () {
  135. return this._polygons.length;
  136. },
  137. },
  138. /**
  139. * Returns the total number of positions in all polygons in the collection.
  140. *
  141. * @memberof ClippingPolygonCollection.prototype
  142. * @type {number}
  143. * @readonly
  144. * @private
  145. */
  146. totalPositions: {
  147. get: function () {
  148. return this._totalPositions;
  149. },
  150. },
  151. /**
  152. * Returns a texture containing the packed computed spherical extents for each polygon
  153. *
  154. * @memberof ClippingPolygonCollection.prototype
  155. * @type {Texture}
  156. * @readonly
  157. * @private
  158. */
  159. extentsTexture: {
  160. get: function () {
  161. return this._extentsTexture;
  162. },
  163. },
  164. /**
  165. * Returns the number of packed extents, which can be fewer than the number of polygons.
  166. *
  167. * @memberof ClippingPolygonCollection.prototype
  168. * @type {number}
  169. * @readonly
  170. * @private
  171. */
  172. extentsCount: {
  173. get: function () {
  174. return this._extentsCount;
  175. },
  176. },
  177. /**
  178. * Returns the number of pixels needed in the texture containing the packed computed spherical extents for each polygon.
  179. *
  180. * @memberof ClippingPolygonCollection.prototype
  181. * @type {number}
  182. * @readonly
  183. * @private
  184. */
  185. pixelsNeededForExtents: {
  186. get: function () {
  187. return this.length; // With an RGBA texture, each pixel contains min/max latitude and longitude.
  188. },
  189. },
  190. /**
  191. * Returns the number of pixels needed in the texture containing the packed polygon positions.
  192. *
  193. * @memberof ClippingPolygonCollection.prototype
  194. * @type {number}
  195. * @readonly
  196. * @private
  197. */
  198. pixelsNeededForPolygonPositions: {
  199. get: function () {
  200. // In an RG FLOAT texture, each polygon position is 2 floats packed to a RG.
  201. // Each polygon has a 1-pixel header + 2 pixels for individual extents + the list of positions
  202. return this.totalPositions + 3 * this.length;
  203. },
  204. },
  205. /**
  206. * Returns a texture containing the computed signed distance of each polygon.
  207. *
  208. * @memberof ClippingPolygonCollection.prototype
  209. * @type {Texture}
  210. * @readonly
  211. * @private
  212. */
  213. clippingTexture: {
  214. get: function () {
  215. return this._signedDistanceTexture;
  216. },
  217. },
  218. /**
  219. * A reference to the ClippingPolygonCollection's owner, if any.
  220. *
  221. * @memberof ClippingPolygonCollection.prototype
  222. * @readonly
  223. * @private
  224. */
  225. owner: {
  226. get: function () {
  227. return this._owner;
  228. },
  229. },
  230. /**
  231. * Returns a number encapsulating the state for this ClippingPolygonCollection.
  232. *
  233. * Clipping mode is encoded in the sign of the number, which is just the total position count.
  234. * If this value changes, then shader regeneration is necessary.
  235. *
  236. * @memberof ClippingPolygonCollection.prototype
  237. * @returns {number} A Number that describes the ClippingPolygonCollection's state.
  238. * @readonly
  239. * @private
  240. */
  241. clippingPolygonsState: {
  242. get: function () {
  243. return this.inverse ? -this.extentsCount : this.extentsCount;
  244. },
  245. },
  246. });
  247. /**
  248. * Adds the specified {@link ClippingPolygon} to the collection to be used to selectively disable rendering
  249. * on the inside of each polygon. Use {@link ClippingPolygonCollection#unionClippingRegions} to modify
  250. * how modify the clipping behavior of multiple polygons.
  251. *
  252. * @param {ClippingPolygon} polygon The ClippingPolygon to add to the collection.
  253. * @returns {ClippingPolygon} The added ClippingPolygon.
  254. *
  255. * @example
  256. * const polygons = new Cesium.ClippingPolygonCollection();
  257. *
  258. * const positions = Cesium.Cartesian3.fromRadiansArray([
  259. * -1.3194369277314022,
  260. * 0.6988062530900625,
  261. * -1.31941,
  262. * 0.69879,
  263. * -1.3193955980204217,
  264. * 0.6988091578771254,
  265. * -1.3193931220959367,
  266. * 0.698743632490865,
  267. * -1.3194358224045408,
  268. * 0.6987471965556998,
  269. * ]);
  270. *
  271. * polygons.add(new Cesium.ClippingPolygon({
  272. * positions: positions
  273. * }));
  274. *
  275. *
  276. *
  277. * @see ClippingPolygonCollection#remove
  278. * @see ClippingPolygonCollection#removeAll
  279. */
  280. ClippingPolygonCollection.prototype.add = function (polygon) {
  281. //>>includeStart('debug', pragmas.debug);
  282. Check.typeOf.object("polygon", polygon);
  283. //>>includeEnd('debug');
  284. const newPlaneIndex = this._polygons.length;
  285. this._polygons.push(polygon);
  286. this.polygonAdded.raiseEvent(polygon, newPlaneIndex);
  287. return polygon;
  288. };
  289. /**
  290. * Returns the clipping polygon in the collection at the specified index. Indices are zero-based
  291. * and increase as polygons are added. Removing a polygon polygon all polygons after
  292. * it to the left, changing their indices. This function is commonly used with
  293. * {@link ClippingPolygonCollection#length} to iterate over all the polygons
  294. * in the collection.
  295. *
  296. * @param {number} index The zero-based index of the polygon.
  297. * @returns {ClippingPolygon} The ClippingPolygon at the specified index.
  298. *
  299. * @see ClippingPolygonCollection#length
  300. */
  301. ClippingPolygonCollection.prototype.get = function (index) {
  302. //>>includeStart('debug', pragmas.debug);
  303. Check.typeOf.number("index", index);
  304. //>>includeEnd('debug');
  305. return this._polygons[index];
  306. };
  307. /**
  308. * Checks whether this collection contains a ClippingPolygon equal to the given ClippingPolygon.
  309. *
  310. * @param {ClippingPolygon} polygon The ClippingPolygon to check for.
  311. * @returns {boolean} true if this collection contains the ClippingPolygon, false otherwise.
  312. *
  313. * @see ClippingPolygonCollection#get
  314. */
  315. ClippingPolygonCollection.prototype.contains = function (polygon) {
  316. //>>includeStart('debug', pragmas.debug);
  317. Check.typeOf.object("polygon", polygon);
  318. //>>includeEnd('debug');
  319. return this._polygons.some((p) => ClippingPolygon.equals(p, polygon));
  320. };
  321. /**
  322. * Removes the first occurrence of the given ClippingPolygon from the collection.
  323. *
  324. * @param {ClippingPolygon} polygon
  325. * @returns {boolean} <code>true</code> if the polygon was removed; <code>false</code> if the polygon was not found in the collection.
  326. *
  327. * @see ClippingPolygonCollection#add
  328. * @see ClippingPolygonCollection#contains
  329. * @see ClippingPolygonCollection#removeAll
  330. */
  331. ClippingPolygonCollection.prototype.remove = function (polygon) {
  332. //>>includeStart('debug', pragmas.debug);
  333. Check.typeOf.object("polygon", polygon);
  334. //>>includeEnd('debug');
  335. const polygons = this._polygons;
  336. const index = polygons.findIndex((p) => ClippingPolygon.equals(p, polygon));
  337. if (index === -1) {
  338. return false;
  339. }
  340. polygons.splice(index, 1);
  341. this.polygonRemoved.raiseEvent(polygon, index);
  342. return true;
  343. };
  344. /**
  345. * Computes padded extents for a polygon's bounding rectangle, clamped to valid spherical ranges.
  346. *
  347. * @param {Rectangle} extents The original spherical extents to pad.
  348. * @param {number} padding A multiplier applied to the extents' width and height to determine the padding amount.
  349. * @param {Rectangle} [result] An optional rectangle to store the result in.
  350. * @returns {Rectangle} The padded and clamped rectangle.
  351. *
  352. * @private
  353. */
  354. function computePaddedExtents(extents, padding, result) {
  355. const height = Math.max(extents.height * padding, 0);
  356. const width = Math.max(extents.width * padding, 0);
  357. const paddedExtents = Rectangle.clone(extents, result);
  358. // Pad
  359. paddedExtents.south -= height;
  360. paddedExtents.west -= width;
  361. paddedExtents.north += height;
  362. paddedExtents.east += width;
  363. // Clamp
  364. paddedExtents.south = Math.max(paddedExtents.south, -Math.PI);
  365. paddedExtents.west = Math.max(paddedExtents.west, -Math.PI);
  366. paddedExtents.north = Math.min(paddedExtents.north, Math.PI);
  367. paddedExtents.east = Math.min(paddedExtents.east, Math.PI);
  368. return paddedExtents;
  369. }
  370. /**
  371. * @typedef {object} ExtentsResult
  372. * @property {Rectangle[]} extentsList The list of merged padded extents, one per group.
  373. * @property {Map<number, number>} extentsIndexByPolygon A map from polygon index to the index of its group in extentsList.
  374. * @private
  375. */
  376. /**
  377. * Groups nearby ClippingPolygons based on their spherical extents. Overlapping extents will be merged
  378. * into a single encompassing extent. Each Extent will later map into one region in the SignedDistanceTexture (atlas).
  379. *
  380. * Definitions:
  381. * n = number of polygons
  382. * g = number of resulting extents (merged) (g <= n)
  383. * absorb = merge two extents into one
  384. * restart = redo intersection check with previous groups
  385. *
  386. * Algorithm:
  387. * For each polygon we scan existing groups for a first overlap (O(g)),
  388. * then on each subsequent overlap we absorb the group and restart the
  389. * inner scan. Each group can be absorbed at most once per polygon, and
  390. * each restart reduces the group count by one, so the absorb-loop does
  391. * at most O(g) restarts per polygon. Overall: O(n * g) where g ≤ n,
  392. * giving O(n²) worst case when all polygons overlap transitively, but
  393. * typically much better when groups are few and disjoint.
  394. *
  395. * Note: Restarts are required because the new merged bounding box might
  396. * be larger than the two individual that were merged and introduce new
  397. * collisions. Example:
  398. *
  399. * Before merging A and B:
  400. *
  401. * ┌─────────┐
  402. * │ A │
  403. * │ ┌┼────────┐
  404. * └─────────┘│ B │
  405. * ┌────┐ │ │
  406. * │ C │ └────────┘
  407. * └────┘
  408. *
  409. * A overlaps B ✓
  410. * A overlaps C ✗
  411. * B overlaps C ✗
  412. *
  413. * After merging A ∪ B into one extent:
  414. *
  415. * ┌───────────────────┐
  416. * │ │
  417. * │ A ∪ B │
  418. * ├────┐ │
  419. * │ C │ │
  420. * └────┘──────────────┘
  421. *
  422. * (A ∪ B) overlaps C ✓ ← new collision!
  423. *
  424. * @param {ClippingPolygon[]} polygons The array of clipping polygons to compute extents for.
  425. * @param {Rectangle[]} polygonExtentsCache An array of pre-computed spherical extents for each polygon, indexed by polygon index.
  426. * @returns {ExtentsResult} The merged extents and a mapping from polygon indices to their extent group indices.
  427. *
  428. * @private
  429. */
  430. function getExtents(polygons, polygonExtentsCache) {
  431. // Pad extents to avoid floating point error when fragment culling at edges.
  432. const PADDING = 2.5;
  433. // Each group: { extent: padded Rectangle, polygonIndices: number[] }
  434. const groups = [];
  435. const length = polygons.length;
  436. for (let polygonIndex = 0; polygonIndex < length; ++polygonIndex) {
  437. const paddedExtent = computePaddedExtents(
  438. polygonExtentsCache[polygonIndex],
  439. PADDING,
  440. );
  441. // Pass 1: Find the first overlapping group
  442. let targetIdx = -1;
  443. for (let g = 0; g < groups.length; ++g) {
  444. if (
  445. defined(Rectangle.simpleIntersection(groups[g].extent, paddedExtent))
  446. ) {
  447. targetIdx = g;
  448. break;
  449. }
  450. }
  451. if (targetIdx === -1) {
  452. // No overlap — start a new group
  453. groups.push({ extent: paddedExtent, polygonIndices: [polygonIndex] });
  454. } else {
  455. // Overlap - Merge the polygon into the target group
  456. const target = groups[targetIdx];
  457. target.polygonIndices.push(polygonIndex);
  458. Rectangle.union(target.extent, paddedExtent, target.extent);
  459. // Pass 2: Absorb all other groups that overlap the (growing) target
  460. // extent. After each absorption the target grows, so restart the scan
  461. // to catch groups that now transitively overlap.
  462. for (let g = 0; g < groups.length; ++g) {
  463. if (g === targetIdx) {
  464. continue;
  465. }
  466. if (
  467. defined(Rectangle.simpleIntersection(groups[g].extent, target.extent))
  468. ) {
  469. target.polygonIndices.push(...groups[g].polygonIndices);
  470. Rectangle.union(target.extent, groups[g].extent, target.extent);
  471. groups.splice(g, 1);
  472. if (g < targetIdx) {
  473. targetIdx--;
  474. }
  475. g = -1; // restart (loop increment brings it to 0)
  476. }
  477. }
  478. }
  479. }
  480. const extentsList = groups.map((g) => g.extent);
  481. const extentsIndexByPolygon = new Map();
  482. groups.forEach((g, extentIndex) =>
  483. g.polygonIndices.forEach((p) => extentsIndexByPolygon.set(p, extentIndex)),
  484. );
  485. return { extentsList, extentsIndexByPolygon };
  486. }
  487. /**
  488. * Removes all polygons from the collection.
  489. *
  490. * @see ClippingPolygonCollection#add
  491. * @see ClippingPolygonCollection#remove
  492. */
  493. ClippingPolygonCollection.prototype.removeAll = function () {
  494. // Dereference this ClippingPolygonCollection from all ClippingPolygons
  495. const polygons = this._polygons;
  496. const polygonsCount = polygons.length;
  497. for (let i = 0; i < polygonsCount; ++i) {
  498. const polygon = polygons[i];
  499. this.polygonRemoved.raiseEvent(polygon, i);
  500. }
  501. this._polygons = [];
  502. };
  503. function packPolygonsAsFloats(clippingPolygonCollection) {
  504. const polygonsFloat32View = clippingPolygonCollection._float32View;
  505. const extentsFloat32View = clippingPolygonCollection._extentsFloat32View;
  506. const polygons = clippingPolygonCollection._polygons;
  507. /**
  508. * Pre-calculate all polygon spherical extents as it an expensive operation
  509. * @type {ReadonlyArray<Rectangle>}
  510. * */
  511. const polygonExtentsCache = polygons.map((polygon) =>
  512. polygon.computeSphericalExtents(),
  513. );
  514. const { extentsList, extentsIndexByPolygon } = getExtents(
  515. polygons,
  516. polygonExtentsCache,
  517. );
  518. // Polygons are packed sequentially (ordered by extentsIndex) into polygonsFloat32View as follows:
  519. // For each polygon:
  520. // [0] vertexCount - the number of vertices in the polygon
  521. // [1] extentsIndex - index into the extents texture for this polygon's bounding rectangle
  522. // [2] south - southern boundary of the individual polygon extent (radians)
  523. // [3] west - western boundary of the individual polygon extent (radians)
  524. // [4] latitudeRange - (north - south) for the individual polygon extent
  525. // [5] longitudeRange - (east - west) for the individual polygon extent
  526. // [6..6+2*vertexCount-1] pairs of (latitude, longitude) for each vertex,
  527. // computed as fastApproximateAtan2 values to match the shader
  528. // Sort polygon indices by extentsIndex so polygons sharing the same extent are packed together
  529. // Can enable optimizations in the shader
  530. const sortedPolygonIndices = Array.from(polygons.keys()).sort(
  531. (a, b) => extentsIndexByPolygon.get(a) - extentsIndexByPolygon.get(b),
  532. );
  533. let floatIndex = 0;
  534. for (const polygonIndex of sortedPolygonIndices) {
  535. const polygon = polygons[polygonIndex];
  536. // Pack the length of the polygon into the polygon texture array buffer
  537. const length = polygon.length;
  538. polygonsFloat32View[floatIndex++] = length;
  539. polygonsFloat32View[floatIndex++] = extentsIndexByPolygon.get(polygonIndex);
  540. // Pack the individual polygon extent
  541. const polygonExtent = polygonExtentsCache[polygonIndex];
  542. polygonsFloat32View[floatIndex++] = polygonExtent.south;
  543. polygonsFloat32View[floatIndex++] = polygonExtent.west;
  544. polygonsFloat32View[floatIndex++] =
  545. polygonExtent.north - polygonExtent.south;
  546. polygonsFloat32View[floatIndex++] = polygonExtent.east - polygonExtent.west;
  547. // Pack the polygon positions into the polygon texture array buffer
  548. for (let i = 0; i < length; ++i) {
  549. const spherePoint = polygon.positions[i];
  550. // Project into plane with vertical for latitude
  551. const magXY = Math.hypot(spherePoint.x, spherePoint.y);
  552. // Use fastApproximateAtan2 for alignment with shader
  553. const latitudeApproximation = CesiumMath.fastApproximateAtan2(
  554. magXY,
  555. spherePoint.z,
  556. );
  557. const longitudeApproximation = CesiumMath.fastApproximateAtan2(
  558. spherePoint.x,
  559. spherePoint.y,
  560. );
  561. polygonsFloat32View[floatIndex++] = latitudeApproximation;
  562. polygonsFloat32View[floatIndex++] = longitudeApproximation;
  563. }
  564. }
  565. // Extents are packed sequentially into extentsFloat32View as follows:
  566. // For each extent (maps to one RGBA pixel in the extents texture):
  567. // [0] south - the southern boundary of the bounding rectangle (radians)
  568. // [1] west - the western boundary of the bounding rectangle (radians)
  569. // [2] latitudeRangeInverse - 1.0 / (north - south)
  570. // [3] longitudeRangeInverse - 1.0 / (east - west)
  571. let extentsFloatIndex = 0;
  572. for (const extents of extentsList) {
  573. const longitudeRangeInverse = 1.0 / (extents.east - extents.west);
  574. const latitudeRangeInverse = 1.0 / (extents.north - extents.south);
  575. extentsFloat32View[extentsFloatIndex++] = extents.south;
  576. extentsFloat32View[extentsFloatIndex++] = extents.west;
  577. extentsFloat32View[extentsFloatIndex++] = latitudeRangeInverse;
  578. extentsFloat32View[extentsFloatIndex++] = longitudeRangeInverse;
  579. }
  580. clippingPolygonCollection._extentsCount = extentsList.length;
  581. }
  582. const textureResolutionScratch = new Cartesian2();
  583. /**
  584. * Called when {@link Viewer} or {@link CesiumWidget} render the scene to
  585. * build the resources for clipping polygons.
  586. * <p>
  587. * Do not call this function directly.
  588. * </p>
  589. * @private
  590. * @throws {RuntimeError} ClippingPolygonCollections are only supported for WebGL 2
  591. */
  592. ClippingPolygonCollection.prototype.update = function (frameState) {
  593. const context = frameState.context;
  594. if (!ClippingPolygonCollection.isSupported(frameState)) {
  595. throw new RuntimeError(
  596. "ClippingPolygonCollections are only supported for WebGL 2.",
  597. );
  598. }
  599. if (this.debugShowDistanceTexture && defined(this._signedDistanceTexture)) {
  600. if (!defined(this.debugCommand)) {
  601. this.debugCommand = createDebugCommand(
  602. this._signedDistanceTexture,
  603. frameState.context,
  604. );
  605. }
  606. frameState.commandList.push(this.debugCommand);
  607. }
  608. // 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.
  609. const totalPositions = this._polygons.reduce(
  610. (totalPositions, polygon) => totalPositions + polygon.length,
  611. 0,
  612. );
  613. if (totalPositions === this.totalPositions) {
  614. return;
  615. }
  616. this._totalPositions = totalPositions;
  617. // If there are no clipping polygons, there's nothing to update.
  618. if (this.length === 0) {
  619. return;
  620. }
  621. if (defined(this._signedDistanceComputeCommand)) {
  622. this._signedDistanceComputeCommand.canceled = true;
  623. this._signedDistanceComputeCommand = undefined;
  624. }
  625. let polygonsTexture = this._polygonsTexture;
  626. let extentsTexture = this._extentsTexture;
  627. let signedDistanceTexture = this._signedDistanceTexture;
  628. if (defined(polygonsTexture)) {
  629. const currentPixelCount = polygonsTexture.width * polygonsTexture.height;
  630. // Recreate the texture to double current requirement if it isn't big enough or is 4 times larger than it needs to be.
  631. // Optimization note: this isn't exactly the classic resizeable array algorithm
  632. // * not necessarily checking for resize after each add/remove operation
  633. // * random-access deletes instead of just pops
  634. // * alloc ops likely more expensive than demonstrable via big-O analysis
  635. if (
  636. currentPixelCount < this.pixelsNeededForPolygonPositions ||
  637. this.pixelsNeededForPolygonPositions < 0.25 * currentPixelCount
  638. ) {
  639. polygonsTexture.destroy();
  640. polygonsTexture = undefined;
  641. this._polygonsTexture = undefined;
  642. }
  643. }
  644. if (!defined(polygonsTexture)) {
  645. const requiredResolution = ClippingPolygonCollection.getTextureResolution(
  646. polygonsTexture,
  647. this.pixelsNeededForPolygonPositions,
  648. textureResolutionScratch,
  649. );
  650. polygonsTexture = new Texture({
  651. context: context,
  652. width: requiredResolution.x,
  653. height: requiredResolution.y,
  654. pixelFormat: PixelFormat.RG,
  655. pixelDatatype: PixelDatatype.FLOAT,
  656. sampler: Sampler.NEAREST,
  657. flipY: false,
  658. });
  659. this._float32View = new Float32Array(
  660. requiredResolution.x * requiredResolution.y * 2,
  661. );
  662. this._polygonsTexture = polygonsTexture;
  663. }
  664. if (defined(extentsTexture)) {
  665. const currentPixelCount = extentsTexture.width * extentsTexture.height;
  666. // Recreate the texture to double current requirement if it isn't big enough or is 4 times larger than it needs to be.
  667. // Optimization note: this isn't exactly the classic resizeable array algorithm
  668. // * not necessarily checking for resize after each add/remove operation
  669. // * random-access deletes instead of just pops
  670. // * alloc ops likely more expensive than demonstrable via big-O analysis
  671. if (
  672. currentPixelCount < this.pixelsNeededForExtents ||
  673. this.pixelsNeededForExtents < 0.25 * currentPixelCount
  674. ) {
  675. extentsTexture.destroy();
  676. extentsTexture = undefined;
  677. this._extentsTexture = undefined;
  678. }
  679. }
  680. if (!defined(extentsTexture)) {
  681. const requiredResolution = ClippingPolygonCollection.getTextureResolution(
  682. extentsTexture,
  683. this.pixelsNeededForExtents,
  684. textureResolutionScratch,
  685. );
  686. extentsTexture = new Texture({
  687. context: context,
  688. width: requiredResolution.x,
  689. height: requiredResolution.y,
  690. pixelFormat: PixelFormat.RGBA,
  691. pixelDatatype: PixelDatatype.FLOAT,
  692. sampler: Sampler.NEAREST,
  693. flipY: false,
  694. });
  695. this._extentsFloat32View = new Float32Array(
  696. requiredResolution.x * requiredResolution.y * 4,
  697. );
  698. this._extentsTexture = extentsTexture;
  699. }
  700. packPolygonsAsFloats(this);
  701. extentsTexture.copyFrom({
  702. source: {
  703. width: extentsTexture.width,
  704. height: extentsTexture.height,
  705. arrayBufferView: this._extentsFloat32View,
  706. },
  707. });
  708. polygonsTexture.copyFrom({
  709. source: {
  710. width: polygonsTexture.width,
  711. height: polygonsTexture.height,
  712. arrayBufferView: this._float32View,
  713. },
  714. });
  715. if (!defined(signedDistanceTexture)) {
  716. const textureDimensions =
  717. ClippingPolygonCollection.getClippingDistanceTextureResolution(
  718. this,
  719. textureResolutionScratch,
  720. );
  721. signedDistanceTexture = new Texture({
  722. context: context,
  723. width: textureDimensions.x,
  724. height: textureDimensions.y,
  725. pixelFormat: context.webgl2 ? PixelFormat.RED : PixelFormat.LUMINANCE,
  726. pixelDatatype: PixelDatatype.FLOAT,
  727. sampler: new Sampler({
  728. wrapS: TextureWrap.CLAMP_TO_EDGE,
  729. wrapT: TextureWrap.CLAMP_TO_EDGE,
  730. minificationFilter: TextureMinificationFilter.LINEAR,
  731. magnificationFilter: TextureMagnificationFilter.LINEAR,
  732. }),
  733. flipY: false,
  734. });
  735. this._signedDistanceTexture = signedDistanceTexture;
  736. }
  737. this._signedDistanceComputeCommand = createSignedDistanceTextureCommand(this);
  738. };
  739. function createDebugCommand(texture, context) {
  740. const fs =
  741. "uniform highp sampler2D billboard_texture; \n" +
  742. "in vec2 v_textureCoordinates; \n" +
  743. "float getSignedDistance(vec2 uv, highp sampler2D clippingDistance) { \n" +
  744. " float signedDistance = texture(clippingDistance, uv).r; \n" +
  745. " return (signedDistance - 0.5) * 2.0; \n" +
  746. "} \n" +
  747. "void main() \n" +
  748. "{ \n" +
  749. " float dist = texture(billboard_texture, v_textureCoordinates).r; \n" +
  750. " if (dist > 0.5) { \n" +
  751. " out_FragColor = vec4(dist, 0.0, 0.0, 1.0); \n" + // outside
  752. " } else {\n" +
  753. " out_FragColor = vec4(0.0, dist, 0.0, 1.0); \n" + // inside
  754. " } \n" +
  755. "} \n";
  756. const drawCommand = context.createViewportQuadCommand(fs, {
  757. uniformMap: {
  758. billboard_texture: function () {
  759. return texture;
  760. },
  761. },
  762. });
  763. drawCommand.pass = Pass.OVERLAY;
  764. return drawCommand;
  765. }
  766. /**
  767. * Called when {@link Viewer} or {@link CesiumWidget} render the scene to
  768. * build the resources for clipping polygons.
  769. * <p>
  770. * Do not call this function directly.
  771. * </p>
  772. * @private
  773. * @param {FrameState} frameState
  774. */
  775. ClippingPolygonCollection.prototype.queueCommands = function (frameState) {
  776. if (defined(this._signedDistanceComputeCommand)) {
  777. frameState.commandList.push(this._signedDistanceComputeCommand);
  778. }
  779. };
  780. function createSignedDistanceTextureCommand(collection) {
  781. const polygonTexture = collection._polygonsTexture;
  782. const extentsTexture = collection._extentsTexture;
  783. return new ComputeCommand({
  784. fragmentShaderSource: PolygonSignedDistanceFS,
  785. outputTexture: collection._signedDistanceTexture,
  786. uniformMap: {
  787. u_polygonsLength: function () {
  788. return collection.length;
  789. },
  790. u_extentsLength: function () {
  791. return collection.extentsCount;
  792. },
  793. u_extentsTexture: function () {
  794. return extentsTexture;
  795. },
  796. u_polygonTexture: function () {
  797. return polygonTexture;
  798. },
  799. },
  800. persists: false,
  801. owner: collection,
  802. postExecute: () => {
  803. collection._signedDistanceComputeCommand = undefined;
  804. },
  805. });
  806. }
  807. const scratchRectangleTile = new Rectangle();
  808. const scratchRectangleIntersection = new Rectangle();
  809. const scratchRectanglePolygon = new Rectangle();
  810. /**
  811. * Determines the type intersection with the polygons of this ClippingPolygonCollection instance and the specified {@link TileBoundingVolume}.
  812. * @private
  813. *
  814. * @param {object} tileBoundingVolume The volume to determine the intersection with the polygons.
  815. * @param {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid on which the bounding volumes are defined.
  816. * @returns {Intersect} The intersection type: {@link Intersect.OUTSIDE} if the entire volume is not clipped, {@link Intersect.INSIDE}
  817. * if the entire volume should be clipped, and {@link Intersect.INTERSECTING} if the volume intersects the polygons and will partially clipped.
  818. */
  819. ClippingPolygonCollection.prototype.computeIntersectionWithBoundingVolume =
  820. function (tileBoundingVolume, ellipsoid) {
  821. const polygons = this._polygons;
  822. const length = polygons.length;
  823. let intersection = Intersect.OUTSIDE;
  824. if (this.inverse) {
  825. intersection = Intersect.INSIDE;
  826. }
  827. let tileBoundingRectangle = tileBoundingVolume.rectangle;
  828. if (
  829. !defined(tileBoundingRectangle) &&
  830. defined(tileBoundingVolume.boundingVolume?.computeCorners)
  831. ) {
  832. const points = tileBoundingVolume.boundingVolume.computeCorners();
  833. tileBoundingRectangle = Rectangle.fromCartesianArray(
  834. points,
  835. ellipsoid,
  836. scratchRectangleTile,
  837. );
  838. }
  839. if (!defined(tileBoundingRectangle)) {
  840. tileBoundingRectangle = Rectangle.fromBoundingSphere(
  841. tileBoundingVolume.boundingSphere,
  842. ellipsoid,
  843. scratchRectangleTile,
  844. );
  845. }
  846. for (let i = 0; i < length; ++i) {
  847. const polygon = polygons[i];
  848. const polygonBoundingRectangle = polygon.computeRectangle(
  849. scratchRectanglePolygon,
  850. );
  851. const result = Rectangle.simpleIntersection(
  852. tileBoundingRectangle,
  853. polygonBoundingRectangle,
  854. scratchRectangleIntersection,
  855. );
  856. if (defined(result)) {
  857. return Intersect.INTERSECTING;
  858. }
  859. }
  860. return intersection;
  861. };
  862. /**
  863. * Sets the owner for the input ClippingPolygonCollection if there wasn't another owner.
  864. * Destroys the owner's previous ClippingPolygonCollection if setting is successful.
  865. *
  866. * @param {ClippingPolygonCollection} [clippingPolygonsCollection] A ClippingPolygonCollection (or undefined) being attached to an object
  867. * @param {object} owner An Object that should receive the new ClippingPolygonCollection
  868. * @param {string} key The Key for the Object to reference the ClippingPolygonCollection
  869. * @private
  870. */
  871. ClippingPolygonCollection.setOwner = function (
  872. clippingPolygonsCollection,
  873. owner,
  874. key,
  875. ) {
  876. // Don't destroy the ClippingPolygonCollection if it is already owned by newOwner
  877. if (clippingPolygonsCollection === owner[key]) {
  878. return;
  879. }
  880. // Destroy the existing ClippingPolygonCollection, if any
  881. owner[key] = owner[key] && owner[key].destroy();
  882. if (defined(clippingPolygonsCollection)) {
  883. //>>includeStart('debug', pragmas.debug);
  884. if (defined(clippingPolygonsCollection._owner)) {
  885. throw new DeveloperError(
  886. "ClippingPolygonCollection should only be assigned to one object",
  887. );
  888. }
  889. //>>includeEnd('debug');
  890. clippingPolygonsCollection._owner = owner;
  891. owner[key] = clippingPolygonsCollection;
  892. }
  893. };
  894. /**
  895. * Function for checking if the context will allow clipping polygons, which require floating point textures.
  896. *
  897. * @param {Scene|object} scene The scene that will contain clipped objects and clipping textures.
  898. * @returns {boolean} <code>true</code> if the context supports clipping polygons.
  899. */
  900. ClippingPolygonCollection.isSupported = function (scene) {
  901. return scene?.context.webgl2;
  902. };
  903. /**
  904. * Function for getting packed texture resolution.
  905. * If the ClippingPolygonCollection hasn't been updated, returns the resolution that will be
  906. * allocated based on the provided needed pixels.
  907. *
  908. * @param {Texture} texture The texture to be packed.
  909. * @param {number} pixelsNeeded The number of pixels needed based on the current polygon count.
  910. * @param {Cartesian2} result A Cartesian2 for the result.
  911. * @returns {Cartesian2} The required resolution.
  912. * @private
  913. */
  914. ClippingPolygonCollection.getTextureResolution = function (
  915. texture,
  916. pixelsNeeded,
  917. result,
  918. ) {
  919. if (defined(texture)) {
  920. result.x = texture.width;
  921. result.y = texture.height;
  922. return result;
  923. }
  924. const maxSize = ContextLimits.maximumTextureSize;
  925. result.x = Math.min(pixelsNeeded, maxSize);
  926. result.y = Math.ceil(pixelsNeeded / result.x);
  927. // Allocate twice as much space as needed to avoid frequent texture reallocation.
  928. result.y *= 2;
  929. return result;
  930. };
  931. /**
  932. * Function for getting the clipping collection's signed distance texture resolution.
  933. * If the ClippingPolygonCollection hasn't been updated, returns the resolution that will be
  934. * allocated based on the current settings
  935. *
  936. * @param {ClippingPolygonCollection} clippingPolygonCollection The clipping polygon collection
  937. * @param {Cartesian2} result A Cartesian2 for the result.
  938. * @returns {Cartesian2} The required resolution.
  939. * @private
  940. */
  941. ClippingPolygonCollection.getClippingDistanceTextureResolution = function (
  942. clippingPolygonCollection,
  943. result,
  944. ) {
  945. const texture = clippingPolygonCollection.signedDistanceTexture;
  946. if (defined(texture)) {
  947. result.x = texture.width;
  948. result.y = texture.height;
  949. return result;
  950. }
  951. const quality = clippingPolygonCollection.quality;
  952. const baseSize = Math.max(128, Math.ceil(4096 * quality));
  953. result.x = Math.min(ContextLimits.maximumTextureSize, baseSize);
  954. result.y = Math.min(ContextLimits.maximumTextureSize, baseSize);
  955. return result;
  956. };
  957. /**
  958. * Function for getting the clipping collection's extents texture resolution.
  959. * If the ClippingPolygonCollection hasn't been updated, returns the resolution that will be
  960. * allocated based on the current polygon count.
  961. *
  962. * @param {ClippingPolygonCollection} clippingPolygonCollection The clipping polygon collection
  963. * @param {Cartesian2} result A Cartesian2 for the result.
  964. * @returns {Cartesian2} The required resolution.
  965. * @private
  966. */
  967. ClippingPolygonCollection.getClippingExtentsTextureResolution = function (
  968. clippingPolygonCollection,
  969. result,
  970. ) {
  971. const texture = clippingPolygonCollection.extentsTexture;
  972. if (defined(texture)) {
  973. result.x = texture.width;
  974. result.y = texture.height;
  975. return result;
  976. }
  977. return ClippingPolygonCollection.getTextureResolution(
  978. texture,
  979. clippingPolygonCollection.pixelsNeededForExtents,
  980. result,
  981. );
  982. };
  983. /**
  984. * Returns true if this object was destroyed; otherwise, false.
  985. * <br /><br />
  986. * If this object was destroyed, it should not be used; calling any function other than
  987. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  988. *
  989. * @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
  990. *
  991. * @see ClippingPolygonCollection#destroy
  992. */
  993. ClippingPolygonCollection.prototype.isDestroyed = function () {
  994. return false;
  995. };
  996. /**
  997. * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
  998. * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
  999. * <br /><br />
  1000. * Once an object is destroyed, it should not be used; calling any function other than
  1001. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
  1002. * assign the return value (<code>undefined</code>) to the object as done in the example.
  1003. *
  1004. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  1005. *
  1006. *
  1007. * @example
  1008. * clippingPolygons = clippingPolygons && clippingPolygons.destroy();
  1009. *
  1010. * @see ClippingPolygonCollection#isDestroyed
  1011. */
  1012. ClippingPolygonCollection.prototype.destroy = function () {
  1013. if (defined(this._signedDistanceComputeCommand)) {
  1014. this._signedDistanceComputeCommand.canceled = true;
  1015. }
  1016. this._polygonsTexture =
  1017. this._polygonsTexture && this._polygonsTexture.destroy();
  1018. this._extentsTexture = this._extentsTexture && this._extentsTexture.destroy();
  1019. this._signedDistanceTexture =
  1020. this._signedDistanceTexture && this._signedDistanceTexture.destroy();
  1021. return destroyObject(this);
  1022. };
  1023. export default ClippingPolygonCollection;