From 5802e2b95f21c83bda5e441308ff6100d11db371 Mon Sep 17 00:00:00 2001 From: Samuel Vargas Date: Tue, 19 May 2020 14:10:16 -0400 Subject: [PATCH] Add PolygonClipping support --- Apps/Sandcastle/gallery/Clipping Polygon.html | 289 ++++++++++ .../gallery/development/Clipping Polygon.jpg | Bin 0 -> 17910 bytes Source/DataSources/ModelGraphics.js | 13 + Source/DataSources/ModelVisualizer.js | 4 + Source/Renderer/ShaderProgram.js | 1 + Source/Renderer/createUniform.js | 1 + Source/Scene/Batched3DModel3DTileContent.js | 6 + Source/Scene/Cesium3DTile.js | 10 + Source/Scene/Cesium3DTileset.js | 28 +- Source/Scene/ClippingPolygon.js | 534 ++++++++++++++++++ Source/Scene/Globe.js | 14 + Source/Scene/GlobeSurfaceShaderSet.js | 15 +- Source/Scene/GlobeSurfaceTileProvider.js | 139 +++++ Source/Scene/Model.js | 122 ++++ .../Scene/PolygonClippingAccelerationGrid.js | 520 +++++++++++++++++ Source/Scene/getPolygonClippingFunction.js | 384 +++++++++++++ Source/Shaders/GlobeFS.glsl | 6 + Source/Shaders/GlobeVS.glsl | 4 +- Source/ThirdParty/simplify3d.js | 138 +++++ .../PolygonClippingAccelerationGridSpec.js | 155 +++++ 20 files changed, 2378 insertions(+), 5 deletions(-) create mode 100644 Apps/Sandcastle/gallery/Clipping Polygon.html create mode 100644 Apps/Sandcastle/gallery/development/Clipping Polygon.jpg create mode 100644 Source/Scene/ClippingPolygon.js create mode 100644 Source/Scene/PolygonClippingAccelerationGrid.js create mode 100644 Source/Scene/getPolygonClippingFunction.js create mode 100644 Source/ThirdParty/simplify3d.js create mode 100644 Specs/Scene/PolygonClippingAccelerationGridSpec.js diff --git a/Apps/Sandcastle/gallery/Clipping Polygon.html b/Apps/Sandcastle/gallery/Clipping Polygon.html new file mode 100644 index 000000000000..618740051b20 --- /dev/null +++ b/Apps/Sandcastle/gallery/Clipping Polygon.html @@ -0,0 +1,289 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/development/Clipping Polygon.jpg b/Apps/Sandcastle/gallery/development/Clipping Polygon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c81d61c632eaf5712320007b9b0b7b7def17afb GIT binary patch literal 17910 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 z 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 OC87E6MHE&>hyU3~T0vO= literal 0 HcmV?d00001 diff --git a/Source/DataSources/ModelGraphics.js b/Source/DataSources/ModelGraphics.js index a30b00954c88..f16b8af8dd69 100644 --- a/Source/DataSources/ModelGraphics.js +++ b/Source/DataSources/ModelGraphics.js @@ -107,6 +107,7 @@ function ModelGraphics(options) { this._clippingPlanes = undefined; this._clippingPlanesSubscription = undefined; + this._clippingPolygon = undefined; this.merge(defaultValue(options, defaultValue.EMPTY_OBJECT)); } @@ -309,6 +310,13 @@ Object.defineProperties(ModelGraphics.prototype, { * @type {Property|undefined} */ clippingPlanes: createPropertyDescriptor("clippingPlanes"), + + /** + * A property specifying the {@link ClippingPolygon} used to selectively disable rendering the model. + * @memberof ModelGraphics.prototype + * @type {Property} + */ + clippingPolygon: createPropertyDescriptor("clippingPolygon"), }); /** @@ -341,6 +349,7 @@ ModelGraphics.prototype.clone = function (result) { result.nodeTransformations = this.nodeTransformations; result.articulations = this.articulations; result.clippingPlanes = this.clippingPlanes; + result.clippingPolygon = this.clippingPolygon; return result; }; @@ -409,6 +418,10 @@ ModelGraphics.prototype.merge = function (source) { this.clippingPlanes, source.clippingPlanes ); + this.clippingPolygon = defaultValue( + this.clippingPolygon, + source.clippingPolygon + ); var sourceNodeTransformations = source.nodeTransformations; if (defined(sourceNodeTransformations)) { diff --git a/Source/DataSources/ModelVisualizer.js b/Source/DataSources/ModelVisualizer.js index df731bcdb3ed..9d8265f1ebee 100644 --- a/Source/DataSources/ModelVisualizer.js +++ b/Source/DataSources/ModelVisualizer.js @@ -199,6 +199,10 @@ ModelVisualizer.prototype.update = function (time) { modelGraphics._clippingPlanes, time ); + model.clippingPolygon = Property.getValueOrUndefined( + modelGraphics._clippingPolygon, + time + ); model.clampAnimations = Property.getValueOrDefault( modelGraphics._clampAnimations, time, diff --git a/Source/Renderer/ShaderProgram.js b/Source/Renderer/ShaderProgram.js index 2b7f6f4158c4..8d3397ec4bcd 100644 --- a/Source/Renderer/ShaderProgram.js +++ b/Source/Renderer/ShaderProgram.js @@ -500,6 +500,7 @@ function reinitialize(shader) { program, gl.ACTIVE_ATTRIBUTES ); + var uniforms = findUniforms(gl, program); var partitionedUniforms = partitionUniforms(shader, uniforms.uniformsByName); diff --git a/Source/Renderer/createUniform.js b/Source/Renderer/createUniform.js index 7a440bd92bce..72b170080f04 100644 --- a/Source/Renderer/createUniform.js +++ b/Source/Renderer/createUniform.js @@ -218,6 +218,7 @@ UniformSampler.prototype.set = function () { gl.activeTexture(gl.TEXTURE0 + this.textureUnitIndex); var v = this.value; + gl.bindTexture(v._target, v._texture); }; diff --git a/Source/Scene/Batched3DModel3DTileContent.js b/Source/Scene/Batched3DModel3DTileContent.js index 9c0586953e5d..440e45f81869 100644 --- a/Source/Scene/Batched3DModel3DTileContent.js +++ b/Source/Scene/Batched3DModel3DTileContent.js @@ -548,6 +548,12 @@ Batched3DModel3DTileContent.prototype.update = function (tileset, frameState) { : undefined; } + var tilesetClippingPolygon = this._tileset.clippingPolygon; + if (defined(tilesetClippingPolygon)) { + // TODO: This should be behind a dirty flag like clippingPlanes + this._model._clippingPolygon = this._tileset.clippingPolygon; + } + // If the model references a different ClippingPlaneCollection due to the tileset's collection being replaced with a // ClippingPlaneCollection that gives this tile the same clipping status, update the model to use the new ClippingPlaneCollection. if ( diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index feced6246f42..d56c19a17321 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -351,6 +351,7 @@ function Cesium3DTile(tileset, baseResource, header, parent) { this._shouldSelect = false; this._isClipped = true; this._clippingPlanesState = 0; // encapsulates (_isClipped, clippingPlanes.enabled) and number/function + this._clippingPolygonState = 0; // encapsulates (-isClipped, clippingPolygons.enabled) this._debugBoundingVolume = undefined; this._debugContentBoundingVolume = undefined; this._debugViewerRequestVolume = undefined; @@ -1077,6 +1078,7 @@ Cesium3DTile.prototype.unloadContent = function () { this.lastStyleTime = 0.0; this.clippingPlanesDirty = this._clippingPlanesState === 0; + // TODO: Handle clipping polygon this._clippingPlanesState = 0; this._debugColorizeTiles = false; @@ -1153,6 +1155,7 @@ Cesium3DTile.prototype.visibility = function ( var tileset = this._tileset; var clippingPlanes = tileset.clippingPlanes; + // TODO: Handle clipping polygon if (defined(clippingPlanes) && clippingPlanes.enabled) { var intersection = clippingPlanes.computeIntersectionWithBoundingVolume( boundingVolume, @@ -1199,6 +1202,9 @@ Cesium3DTile.prototype.contentVisibility = function (frameState) { var tileset = this._tileset; var clippingPlanes = tileset.clippingPlanes; + + // TODO: handle clipping polygon + if (defined(clippingPlanes) && clippingPlanes.enabled) { var intersection = clippingPlanes.computeIntersectionWithBoundingVolume( boundingVolume, @@ -1603,6 +1609,8 @@ function updateClippingPlanes(tile, tileset) { tile._clippingPlanesState = currentClippingPlanesState; tile.clippingPlanesDirty = true; } + + // HANDLE CLIPPING POLYGON } /** @@ -1618,6 +1626,8 @@ Cesium3DTile.prototype.update = function (tileset, frameState, passOptions) { this._commandsLength = frameState.commandList.length - initCommandLength; this.clippingPlanesDirty = false; // reset after content update + + // handle clipping polygon }; var scratchCommandList = []; diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 45bfd2c01da9..1c5ae9a8a268 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -81,6 +81,7 @@ import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js"; * @param {Boolean} [options.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. * @param {Boolean} [options.loadSiblings=false] When skipLevelOfDetail is true, determines whether siblings of visible tiles are always downloaded during traversal. * @param {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset. + * @param {ClippingPolygon} [options.clippingPolygon] The {@link ClippingPolygon} used to selectively disable rendering the tileset. * @param {ClassificationType} [options.classificationType] Determines whether terrain, 3D Tiles or both will be classified by this tileset. See {@link Cesium3DTileset#classificationType} for details about restrictions and limitations. * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid determining the size and shape of the globe. * @param {Object} [options.pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point attenuation based on geometric error and lighting. @@ -694,6 +695,9 @@ function Cesium3DTileset(options) { this._clippingPlanes = undefined; this.clippingPlanes = options.clippingPlanes; + this._clippingPolygon = undefined; + this.clippingPolygon = options.clippingPolygon; + this._imageBasedLightingFactor = new Cartesian2(1.0, 1.0); Cartesian2.clone( options.imageBasedLightingFactor, @@ -1054,6 +1058,22 @@ Object.defineProperties(Cesium3DTileset.prototype, { }, }, + /** + * The {@link ClippingPolygon} used to selectively disable rendering the tileset. + * + * @memberof Cesium3DTileset.prototype + * + * @type {ClippingPolygon} + */ + clippingPolygon: { + get: function () { + return this._clippingPolygon; + }, + set: function (value) { + this._clippingPolygon = value; + }, + }, + /** * Gets the tileset's properties dictionary object, which contains metadata about per-feature properties. *

@@ -1902,6 +1922,11 @@ Cesium3DTileset.prototype.prePassesUpdate = function (frameState) { clippingPlanes.update(frameState); } + var clippingPolygon = this._clippingPolygon; + if (defined(clippingPolygon)) { + clippingPolygon.update(frameState); + } + if (!defined(this._loadTimestamp)) { this._loadTimestamp = JulianDate.clone(frameState.time); } @@ -1911,8 +1936,7 @@ Cesium3DTileset.prototype.prePassesUpdate = function (frameState) { ); this._skipLevelOfDetail = - this.skipLevelOfDetail && - !defined(this._classificationType) && + this.skipLevelOfDetail & !defined(this._classificationType) && !this._disableSkipLevelOfDetail && !this._allTilesAdditive; diff --git a/Source/Scene/ClippingPolygon.js b/Source/Scene/ClippingPolygon.js new file mode 100644 index 000000000000..9cf070b57ac7 --- /dev/null +++ b/Source/Scene/ClippingPolygon.js @@ -0,0 +1,534 @@ +import PolygonClippingAccelerationGrid from "./PolygonClippingAccelerationGrid.js"; +import defaultValue from "../Core/defaultValue.js"; +import defined from "../Core/defined.js"; +import Cartesian3 from "../Core/Cartesian3.js"; +import Cartesian2 from "../Core/Cartesian2.js"; +import PolygonPipeline from "../Core/PolygonPipeline.js"; +import WindingOrder from "../Core/WindingOrder.js"; +import earcut from "../ThirdParty/earcut-2.2.1.js"; +import DeveloperError from "../Core/DeveloperError.js"; +import Texture from "../Renderer/Texture.js"; +import PixelDatatype from "../Renderer/PixelDatatype.js"; +import Sampler from "../Renderer/Sampler.js"; +import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js"; +import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js"; +import TextureWrap from "../Renderer/TextureWrap.js"; +import PixelFormat from "../Core/PixelFormat.js"; +import Matrix4 from "../Core/Matrix4.js"; +import ContextLimits from "../Renderer/ContextLimits.js"; +import BoundingSphere from "../Core/BoundingSphere.js"; +import Cartographic from "../Core/Cartographic.js"; +import Transforms from "../Core/Transforms.js"; +import simplify3d from "../ThirdParty/simplify3d.js"; + +/** + * + * @param options + * + * @private + */ +function ClippingPolygon(options) { + this._worldToENU = options.worldToENU; + this._union = options.union; + this._enabled = options.enabled; + + var positions = options.positions; + var indices = options.indices; + var splits = options.splits; + + var accelerator = new PolygonClippingAccelerationGrid({ + positions: positions, + indices: indices, + splits: splits, + }); + + var i; + this._minimumZ = Infinity; + for (i = 0; i < positions.length; i += 3) { + this._minimumZ = Math.min(positions[i + 2], this._minimumZ); + } + + // precision hack, if zoomed out the eye space -> enu space conversion + // can cause some triangles inside the clipping polygon to be < minimumZ + // when they're really not, causing visual artifacts until you zoom in + // making the floor -100km from the actual floor fixes all issues + this._minimumZ -= 100000; + + this._accelerator = accelerator; + this._grid = accelerator.grid; + this._overlappingTriangleIndices = accelerator.overlappingTriangleIndices; + this._meshPositions = Float32Array.from(positions); + this._dirty = true; + + var boundingBox = this._accelerator.boundingBox; + this._boundingBox = boundingBox.toClockwiseCartesian2Pairs(); + this._cellDimensions = new Cartesian2( + this._accelerator.cellWidth, + this._accelerator.cellHeight + ); + + this._numRowsAndCols = new Cartesian2( + this._accelerator.numRows, + this._accelerator.numCols + ); + + // data texture limits: + // - 16,777,216 cells are supported (if you make 4096 splits; overkill as 20-32 splits for most cases) + // - 805,306,368 million overlapping triangle indices (32 bit float precision) + // - 805,306,3368 million overlapping triangle vertex positions (32 bit float precision) + + // the 2D grid acceleration texture stores the cell status in the R channel + // the startIndex into the overlappingTriangleIndicesTexture in the G channel + // and the endIndex into the overlappingTriangleIndicesTexture in the B channel + // The number of triangles that a cell recorded as partially overlapping + // can be computed as (endIndex - startIndex) / 3.0 + var gridPixels = this._grid.length / this._accelerator.cellNumElements; + var gridWidthPixels = gridPixels / this._accelerator.numCols; + var gridHeightPixels = gridPixels / this._accelerator.numRows; + this._gridPixelDimensions = new Cartesian2(gridWidthPixels, gridHeightPixels); + + // dimensions for 2D overlappingTriangleIndicesTexture, note that the indices from + // the gridTexture are 1D and need to be converted into 2D rows and columns, + // and then 2D pixel coordinates before doing a look up in this texture. + + var totalOverlappingTriangles = this._overlappingTriangleIndices.length / 3.0; + var maxDimensionPixelSize = ContextLimits.maximumTextureSize; + var colsToCreate = Math.min(totalOverlappingTriangles, maxDimensionPixelSize); + var rowsToCreate = + Math.floor(totalOverlappingTriangles / maxDimensionPixelSize) + 1.0; + + this._overlappingTrianglePixelIndicesDimensions = new Cartesian2( + colsToCreate, + rowsToCreate + ); + + var totalTrianglesInMesh = this._meshPositions.length / 3.0; + colsToCreate = Math.min(totalTrianglesInMesh, maxDimensionPixelSize); + rowsToCreate = Math.floor(totalTrianglesInMesh / maxDimensionPixelSize) + 1.0; + + this._meshPositionPixelDimensions = new Cartesian2( + colsToCreate, + rowsToCreate + ); +} + +/** + * Constructs a clipping mesh used to selectively enable / disable rendering + * inside of the region defined by the clipping mesh. + * + * @param {Object} options Object with the following properties: + * @param {Array.} options.polygonHierarchies An ARRAY of Polygon + * Hierarchies to amalgamate into a single clipping mesh. Holes are supported. + * @param {Number} [options.simplify-0] Tolerance threshold that should be used + * for mesh simplification. Note that over simplification can result in a degenerate + * mesh which will trigger an exception + * @param {Boolean} [options.union-false] If union is TRUE only geometry inside + * the ClippingPolygon will be rendered. Otherwise only geometry outside the + * ClippingPolygon will be rendered. + */ + +ClippingPolygon.fromPolygonHierarchies = function (options) { + var enabled = defaultValue(options.enabled, true); + var polygonHierarchies = options.polygonHierarchies; + var worldToENU = options.worldToENU; + + if (!defined(worldToENU)) { + worldToENU = generateWorldToENUMatrixFromPolygonHierarchies( + polygonHierarchies + ); + } + + var simplify = defaultValue(options.simplify, 0); + var union = defaultValue(options.union, false); + var splits = defaultValue(options.splits, 33); + var amalgamatedMesh = combinePolygonHierarchiesIntoSingleMesh( + polygonHierarchies, + worldToENU, + simplify + ); + + options.positions = amalgamatedMesh.positions; + options.indices = amalgamatedMesh.indices; + options.worldToENU = worldToENU; + options.union = union; + options.simplify = simplify; + options.splits = splits; + options.enabled = enabled; + return new ClippingPolygon(options); +}; + +Object.defineProperties(ClippingPolygon.prototype, { + grid: { + get: function () { + return this._grid; + }, + }, + + gridNumPixels: { + get: function () { + return this._grid.length / this._accelerator.cellNumElements; + }, + }, + + gridPixelDimensions: { + get: function () { + return this._gridPixelDimensions; + }, + }, + + gridTexture: { + get: function () { + return this._gridTexture; + }, + }, + + minimumZ: { + get: function () { + return this._minimumZ; + }, + }, + + meshPositions: { + get: function () { + return this._meshPositions; + }, + }, + + enabled: { + set: function (enable) { + this._enabled = enable; + }, + get: function () { + return this._enabled; + }, + }, + + union: { + set: function (v) { + this._dirty = true; + this._dirty = v; + }, + + get: function () { + return this._union; + }, + }, + + meshPositionsTexture: { + get: function () { + return this._meshPositionTexture; + }, + }, + + meshPositionPixelDimensions: { + get: function () { + return this._meshPositionPixelDimensions; + }, + }, + + overlappingTriangleIndices: { + get: function () { + return this._overlappingTriangleIndices; + }, + }, + + overlappingTrianglePixelIndicesDimensions: { + get: function () { + return this._overlappingTrianglePixelIndicesDimensions; + }, + }, + + overlappingTriangleIndicesTexture: { + get: function () { + return this._overlappingTriangleIndicesTexture; + }, + }, + + boundingBox: { + get: function () { + return this._boundingBox; + }, + }, + + cellDimensions: { + get: function () { + return this._cellDimensions; + }, + }, + + numRowsAndCols: { + get: function () { + return this._numRowsAndCols; + }, + }, + + worldToENU: { + get: function () { + return this._worldToENU; + }, + }, +}); + +ClippingPolygon.prototype.update = function (frameState) { + if (!this._dirty) { + return; + } + + this._dirty = false; + var context = frameState.context; + + if (!context.floatingPointTexture) { + throw new DeveloperError( + "OES_texture_float or WebGL2 required to use ClippingPolygon" + ); + } + + this._gridTexture = new Texture({ + context: context, + width: this.gridPixelDimensions.x, + height: this.gridPixelDimensions.y, + pixelFormat: PixelFormat.RGB, + pixelDatatype: PixelDatatype.FLOAT, + sampler: Sampler.NEAREST, + wrapS: TextureWrap.CLAMP_TO_EDGE, + wrapT: TextureWrap.CLAMP_TO_EDGE, + minificationFilter: TextureMinificationFilter.NEAREST, + magnificationFilter: TextureMagnificationFilter.NEAREST, + }); + + this._gridTexture.copyFrom({ + width: this.gridPixelDimensions.x, + height: this.gridPixelDimensions.y, + arrayBufferView: this.grid, + }); + + this._meshPositionTexture = new Texture({ + context: context, + width: this.meshPositionPixelDimensions.x, + height: this.meshPositionPixelDimensions.y, + pixelFormat: PixelFormat.RGB, + pixelDatatype: PixelDatatype.FLOAT, + sampler: Sampler.NEAREST, + wrapS: TextureWrap.CLAMP_TO_EDGE, + wrapT: TextureWrap.CLAMP_TO_EDGE, + minificationFilter: TextureMinificationFilter.NEAREST, + magnificationFilter: TextureMagnificationFilter.NEAREST, + }); + + this._meshPositionTexture.copyFrom({ + width: this.meshPositionPixelDimensions.x, + height: this.meshPositionPixelDimensions.y, + arrayBufferView: this.meshPositions, + }); + + this._overlappingTriangleIndicesTexture = new Texture({ + context: context, + width: this.overlappingTrianglePixelIndicesDimensions.x, + height: this.overlappingTrianglePixelIndicesDimensions.y, + pixelFormat: PixelFormat.RGB, + pixelDatatype: PixelDatatype.FLOAT, + sampler: Sampler.NEAREST, + wrapS: TextureWrap.CLAMP_TO_EDGE, + wrapT: TextureWrap.CLAMP_TO_EDGE, + minificationFilter: TextureMinificationFilter.NEAREST, + magnificationFilter: TextureMagnificationFilter.NEAREST, + }); + + this._overlappingTriangleIndicesTexture.copyFrom({ + width: this.overlappingTrianglePixelIndicesDimensions.x, + height: this.overlappingTrianglePixelIndicesDimensions.y, + arrayBufferView: this.overlappingTriangleIndices, + }); +}; + +/** + * @param {Array.} hierarchies + * @param {Matrix4} worldToENU matrix + * @param {Number} simplify Simplification amount + * @return {Object} An object containing the vertex positions plus indices + */ + +function combinePolygonHierarchiesIntoSingleMesh( + hierarchies, + worldToENU, + simplify +) { + // convert each polygon into mesh(es) + var i, j; + var polygonHierarchiesAsMeshes = []; + for (i = 0; i < hierarchies.length; ++i) { + var asMeshes = convertPolygonHierarchyIntoMesh( + hierarchies[i], + worldToENU, + simplify + ); + polygonHierarchiesAsMeshes.push(asMeshes); + } + + // amalgamate the meshes into a single polygon, updating indices as we go + var positions = []; + var indices = []; + + for (i = 0; i < polygonHierarchiesAsMeshes.length; ++i) { + var mesh = polygonHierarchiesAsMeshes[i]; + + for (j = 0; j < mesh.length; ++j) { + var m = mesh[j]; + + var k; + for (k = 0; k < m.indices.length; ++k) { + indices.push(m.indices[k] + positions.length / 3.0); + } + + positions = positions.concat(m.positions); + } + } + + if (indices.length < 3) { + throw new DeveloperError( + "Degenerate clipping mesh generated, try reducing or disabling simplification." + ); + } + + return { + positions: positions, + indices: indices, + }; +} + +function convertPolygonHierarchyIntoMesh( + polygonHierarchy, + worldToENU, + simplify +) { + var holes = defaultValue(polygonHierarchy.holes, []); + var visitHoles = holes.length > 0 ? [].concat(holes) : []; + var isHole = true; + var i, j; + var mesh; + var positions; + var holeIndices; + + var meshes = [ + [ + { + positions: polygonHierarchy.positions, + isHole: false, + }, + ], + ]; + + // collapse the polygonHierarchy into a 2D array, where each + // element is a mesh and the mesh contains the positions that + // form the mesh (plus its non-recursive holes) + + // collapse the PolygonHierarchy into a 1D array + var meshIndex = 0; + do { + if (!defined(meshes[meshIndex])) { + meshes[meshIndex] = []; + } + + mesh = meshes[meshIndex]; + var childCount = visitHoles.length; + + for (i = 0; i < childCount; ++i) { + var hole = visitHoles.splice(0, 1)[0]; + var childHoles = defined(hole.holes) ? [].concat(hole.holes) : []; + visitHoles = visitHoles.concat(childHoles); + mesh.push({ + positions: hole.positions, + isHole: isHole, + }); + } + + isHole = !isHole; + meshIndex += 1; + } while (visitHoles.length > 0); + + var flatPolygon; + var positionsAndIndicesForEarcut = []; + + // translate & simplify each mesh + for (i = 0; i < meshes.length; ++i) { + var transformedPositions = []; + holeIndices = []; + mesh = meshes[i]; + for (j = 0; j < mesh.length; ++j) { + flatPolygon = mesh[j]; + positions = defined(flatPolygon.positions) + ? [].concat(flatPolygon.positions) + : []; + var translatedPositions = []; + var k; + for (k = 0; k < positions.length; ++k) { + var p = Cartesian3.clone(positions[k]); + Matrix4.multiplyByPoint(worldToENU, p, p); + translatedPositions.push(p); + } + + var posWinding = PolygonPipeline.computeWindingOrder2D( + translatedPositions + ); + if (posWinding === WindingOrder.CLOCKWISE) { + translatedPositions.reverse(); + } + + if (simplify > 0) { + translatedPositions = simplify3d(translatedPositions, simplify); + } + + if (flatPolygon.isHole) { + holeIndices.push(transformedPositions.length); + } + + transformedPositions = transformedPositions.concat(translatedPositions); + } + + positionsAndIndicesForEarcut.push({ + positions: transformedPositions, + holeIndices: holeIndices, + }); + } + + var triangulatedMeshes = []; + + for (i = 0; i < positionsAndIndicesForEarcut.length; ++i) { + positions = positionsAndIndicesForEarcut[i].positions; + holeIndices = positionsAndIndicesForEarcut[i].holeIndices; + + var flatPositions = Cartesian3.packArray(positions); + triangulatedMeshes.push({ + positions: flatPositions, + indices: earcut(flatPositions, holeIndices, 3), + }); + } + + return triangulatedMeshes; +} + +/** + * @param polygonHierarchies Array An array of ECEF positions + * @returns Matrix4 a worldToENU matrix + * + * @private + */ + +function generateWorldToENUMatrixFromPolygonHierarchies(polygonHierarchies) { + var i; + var allPoints = []; + for (i = 0; i < polygonHierarchies.length; ++i) { + var positions = polygonHierarchies[i].positions; + allPoints = allPoints.concat(positions); + } + + var boundingSphere = BoundingSphere.fromPoints(allPoints); + var cartographic = Cartographic.fromCartesian(boundingSphere.center); + cartographic.height = 0; + var origin = Cartographic.toCartesian(cartographic); + var enu = Transforms.eastNorthUpToFixedFrame(origin); + + return Matrix4.inverse(enu, new Matrix4()); +} + +export default ClippingPolygon; diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 9d34422954c9..3dd5869f01d3 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -383,6 +383,20 @@ Object.defineProperties(Globe.prototype, { this._surface.tileProvider.clippingPlanes = value; }, }, + /** + * A property specifying a {@link ClippingPolygon} used to selectively disable rendering inside or outside of the given mesh. + * + * @memberof Globe.prototype + * @type {ClippingPolygon} + */ + clippingPolygon: { + get: function () { + return this._surface.tileProvider.clippingPolygon; + }, + set: function (value) { + this._surface.tileProvider.clippingPolygon = 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/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index df662a8f69c2..852c9f35b3d8 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -4,6 +4,7 @@ import TerrainQuantization from "../Core/TerrainQuantization.js"; import ShaderProgram from "../Renderer/ShaderProgram.js"; import getClippingFunction from "./getClippingFunction.js"; import SceneMode from "./SceneMode.js"; +import getClippingPolygonFunction from "./getPolygonClippingFunction.js"; function GlobeSurfaceShader( numberOfDayTextures, @@ -94,7 +95,9 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) { var useWebMercatorProjection = options.useWebMercatorProjection; var enableFog = options.enableFog; var enableClippingPlanes = options.enableClippingPlanes; + var enableClippingPolygon = options.enableClippingPolygon; var clippingPlanes = options.clippingPlanes; + var clippingPolygon = options.clippingPolygon; var clippedByBoundaries = options.clippedByBoundaries; var hasImageryLayerCutout = options.hasImageryLayerCutout; var colorCorrect = options.colorCorrect; @@ -156,8 +159,10 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) { (highlightFillTile << 24) | (colorToAlpha << 25) | (showUndergroundColor << 26) | - (translucent << 27) | - (applyDayNightAlpha << 28); + (enableClippingPolygon << 27); + (showUndergroundColor << 28) | + (translucent << 29) | + (applyDayNightAlpha << 30); var currentClippingShaderState = 0; if (defined(clippingPlanes) && clippingPlanes.length > 0) { @@ -285,6 +290,12 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) { fs.defines.push("ENABLE_CLIPPING_PLANES"); } + if (enableClippingPolygon) { + vs.defines.push("ENABLE_CLIPPING_POLYGON"); + fs.defines.push("ENABLE_CLIPPING_POLYGON"); + fs.sources.unshift(getClippingPolygonFunction(clippingPolygon.union)); + } + if (colorCorrect) { fs.defines.push("COLOR_CORRECT"); } diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index fe9e2bc7eed9..ea6a2e87e55c 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -166,6 +166,14 @@ function GlobeSurfaceTileProvider(options) { */ this._clippingPlanes = undefined; + /** + * A property specifying a {@alink ClippingPolygon} used to selectively + * disable rendering inside or outside of the clipping polygon + * @type {ClippingPolygon} + * @private + */ + this._clippingPolygon = undefined; + /** * A property specifying a {@link Rectangle} used to selectively limit terrain and imagery rendering. * @type {Rectangle} @@ -315,6 +323,24 @@ Object.defineProperties(GlobeSurfaceTileProvider.prototype, { ClippingPlaneCollection.setOwner(value, this, "_clippingPlanes"); }, }, + + /** + * The {@link ClippingPolygon} used to selectively enable or disable + * rendering inside of a specific region + * + * @type {ClippingPlaneCollection} + * + * @private + */ + clippingPolygon: { + get: function () { + return this._clippingPolygon; + }, + set: function (value) { + this._clippingPolygon = value; + //ClippingPolygon.setOwner(value, this, "_clippingPolygon"); + }, + }, }); function sortTileImageryByLayerIndex(a, b) { @@ -404,6 +430,12 @@ GlobeSurfaceTileProvider.prototype.beginUpdate = function (frameState) { if (defined(clippingPlanes) && clippingPlanes.enabled) { clippingPlanes.update(frameState); } + + var clippingPolygon = this._clippingPolygon; + if (defined(clippingPolygon)) { + clippingPolygon.update(frameState); + } + this._usedDrawCommands = 0; this._hasLoadedTilesThisFrame = false; @@ -1489,6 +1521,8 @@ GlobeSurfaceTileProvider.prototype._onLayerShownOrHidden = function ( }; var scratchClippingPlaneMatrix = new Matrix4(); +var scratchClippingPolygonEyeToWorldMatrix = new Matrix4(); + function createTileUniformMap(frameState, globeSurfaceTileProvider) { var uniformMap = { u_initialColor: function () { @@ -1634,6 +1668,95 @@ function createTileUniformMap(frameState, globeSurfaceTileProvider) { style.alpha = this.properties.clippingPlanesEdgeWidth; return style; }, + + u_clippingPolygonAccelerationGrid: function () { + var clippingPolygon = globeSurfaceTileProvider._clippingPolygon; + if (defined(clippingPolygon) && defined(clippingPolygon.gridTexture)) { + return clippingPolygon.gridTexture; + } + }, + + u_clippingPolygonAccelerationGridPixelDimensions: function () { + var clippingPolygon = globeSurfaceTileProvider._clippingPolygon; + if ( + defined(clippingPolygon) && + defined(clippingPolygon.gridPixelDimensions) + ) { + return clippingPolygon.gridPixelDimensions; + } + }, + + u_clippingPolygonMeshPositions: function () { + var clippingPolygon = globeSurfaceTileProvider._clippingPolygon; + if (defined(clippingPolygon) && defined(clippingPolygon.meshPositions)) { + return clippingPolygon.meshPositionsTexture; + } + }, + + u_clippingPolygonMeshPositionPixelDimensions: function () { + var clippingPolygon = globeSurfaceTileProvider._clippingPolygon; + if ( + defined(clippingPolygon) && + defined(clippingPolygon.meshPositionPixelDimensions) + ) { + return clippingPolygon.meshPositionPixelDimensions; + } + }, + + u_clippingPolygonOverlappingTriangleIndices: function () { + var clippingPolygon = globeSurfaceTileProvider._clippingPolygon; + if ( + defined(clippingPolygon) && + defined(clippingPolygon.overlappingTriangleIndicesTexture) + ) { + return clippingPolygon.overlappingTriangleIndicesTexture; + } + }, + + u_clippingPolygonOverlappingTrianglePixelIndicesDimensions: function () { + var clippingPolygon = globeSurfaceTileProvider._clippingPolygon; + if ( + defined(clippingPolygon) && + defined(clippingPolygon.overlappingTrianglePixelIndicesDimensions) + ) { + return clippingPolygon.overlappingTrianglePixelIndicesDimensions; + } + }, + + u_clippingPolygonBoundingBox: function () { + var clippingPolygon = globeSurfaceTileProvider._clippingPolygon; + if (defined(clippingPolygon) && defined(clippingPolygon.boundingBox)) { + return clippingPolygon.boundingBox; + } + }, + + u_clippingPolygonCellDimensions: function () { + var clippingPolygon = globeSurfaceTileProvider._clippingPolygon; + if (defined(clippingPolygon) && defined(clippingPolygon.cellDimensions)) { + return clippingPolygon.cellDimensions; + } + }, + + u_clippingPolygonMinimumZ: function () { + var clippingPolygon = globeSurfaceTileProvider._clippingPolygon; + if (defined(clippingPolygon) && defined(clippingPolygon.minimumZ)) { + return clippingPolygon.minimumZ; + } + }, + + u_clippingPolygonEyeToWorldToENU: function () { + var clippingPolygon = globeSurfaceTileProvider._clippingPolygon; + if (defined(clippingPolygon) && defined(clippingPolygon.worldToENU)) { + var eyeToWorld = frameState.context.uniformState.inverseView3D; + var eyeToWorldToENU = Matrix4.multiply( + clippingPolygon.worldToENU, + eyeToWorld, + scratchClippingPolygonEyeToWorldMatrix + ); + return eyeToWorldToENU; + } + }, + u_minimumBrightness: function () { return frameState.fog.minimumBrightness; }, @@ -1901,6 +2024,7 @@ var surfaceShaderSetOptionsScratch = { useWebMercatorProjection: undefined, enableFog: undefined, enableClippingPlanes: undefined, + enableClippingPolygon: undefined, clippingPlanes: undefined, clippedByBoundaries: undefined, hasImageryLayerCutout: undefined, @@ -2023,6 +2147,10 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) { --maxTextures; } + if (defined(tileProvider.clippingPolygon)) { + --maxTextures; + } + maxTextures -= globeTranslucencyState.numberOfTextureUniforms; var mesh = surfaceTile.renderedMesh; @@ -2542,6 +2670,15 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) { uniformMapProperties.clippingPlanesEdgeWidth = clippingPlanes.edgeWidth; } + // update clipping polygon + var clippingPolygon = tileProvider._clippingPolygon; + var clippingPolygonEnabled = + defined(clippingPolygon) && clippingPolygon.enabled; + + if (defined(tileProvider.uniformMap)) { + uniformMap = combine(uniformMap, tileProvider.uniformMap); + } + surfaceShaderSetOptions.numberOfDayTextures = numberOfDayTextures; surfaceShaderSetOptions.applyBrightness = applyBrightness; surfaceShaderSetOptions.applyContrast = applyContrast; @@ -2553,7 +2690,9 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) { surfaceShaderSetOptions.applySplit = applySplit; surfaceShaderSetOptions.enableFog = applyFog; surfaceShaderSetOptions.enableClippingPlanes = clippingPlanesEnabled; + surfaceShaderSetOptions.enableClippingPolygon = clippingPolygonEnabled; surfaceShaderSetOptions.clippingPlanes = clippingPlanes; + surfaceShaderSetOptions.clippingPolygon = clippingPolygon; surfaceShaderSetOptions.hasImageryLayerCutout = applyCutout; surfaceShaderSetOptions.colorCorrect = colorCorrect; surfaceShaderSetOptions.highlightFillTile = highlightFillTile; diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index f337048d58c4..fa4a3f51b934 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -75,8 +75,10 @@ import processModelMaterialsCommon from "./processModelMaterialsCommon.js"; import processPbrMaterials from "./processPbrMaterials.js"; import SceneMode from "./SceneMode.js"; import ShadowMode from "./ShadowMode.js"; +import getPolygonClippingFunction from "./getPolygonClippingFunction.js"; var boundingSphereCartesian3Scratch = new Cartesian3(); +var scratchClippingPolygonEyeToWorldMatrix = new Matrix4(); var ModelState = ModelUtility.ModelState; @@ -210,6 +212,7 @@ var uriToGuid = {}; * @param {Color} [options.silhouetteColor=Color.RED] The silhouette color. If more than 256 models have silhouettes enabled, there is a small chance that overlapping models will have minor artifacts. * @param {Number} [options.silhouetteSize=0.0] The size of the silhouette in pixels. * @param {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the model. + * @param {ClippingPolygon} [options.clippingPolygon] The {@link ClippingPolygon} used to selectively disable rendering the model. * @param {Boolean} [options.dequantizeInShader=true] Determines if a {@link https://github.com/google/draco|Draco} encoded model is dequantized on the GPU. This decreases total memory usage for encoded models. * @param {Cartesian2} [options.imageBasedLightingFactor=Cartesian2(1.0, 1.0)] Scales diffuse and specular image-based lighting from the earth, sky, atmosphere and star skybox. * @param {Cartesian3} [options.lightColor] The light color when shading the model. When undefined the scene's light color is used instead. @@ -1079,6 +1082,27 @@ Object.defineProperties(Model.prototype, { }, }, + /** + * The {@link ClippingPolygon} used to selectively disable rendering the model. + * + * @memberof Model.prototype + * + * @type {ClippingPolygon} + */ + clippingPolygon: { + get: function () { + return this._clippingPolygon; + }, + set: function (value) { + if (value === this._clippingPolygon) { + return; + } + this._clippingPolygon = value; + // Handle destroying, checking of unknown, checking for existing ownership + //ClippingPlaneCollection.setOwner(value, this, "_clippingPlanes"); + }, + }, + /** * @private */ @@ -1289,6 +1313,11 @@ function isClippingEnabled(model) { ); } +function isClippingPolygonEnabled(model) { + var clippingPolygon = model._clippingPolygon; + return defined(clippingPolygon) && clippingPolygon.enabled; +} + /** * Determines if silhouettes are supported. * @@ -2544,6 +2573,7 @@ function recreateProgram(programToCreate, model, context) { var clippingPlaneCollection = model.clippingPlanes; var addClippingPlaneCode = isClippingEnabled(model); + var clippingPolygon = model.clippingPolygon; var vs = shaders[program.vertexShader]; var fs = shaders[program.fragmentShader]; @@ -2559,6 +2589,11 @@ function recreateProgram(programToCreate, model, context) { if (isColorShadingEnabled(model)) { finalFS = Model._modifyShaderForColor(finalFS); } + + if (isClippingPolygonEnabled(model)) { + finalFS = modifyShaderForClippingPolygon(finalFS, clippingPolygon, context); + } + if (addClippingPlaneCode) { finalFS = modifyShaderForClippingPlanes( finalFS, @@ -3877,6 +3912,58 @@ function createCommand(model, gltfNode, runtimeNode, context, scene3DOnly) { gltf_luminanceAtZenith: createLuminanceAtZenithFunction(model), }); + var clippingPolygon = model._clippingPolygon; + if (defined(clippingPolygon)) { + uniformMap = combine(uniformMap, { + u_clippingPolygonBoundingBox: function () { + return clippingPolygon.boundingBox; + }, + + u_clippingPolygonCellDimensions: function () { + return clippingPolygon.cellDimensions; + }, + + u_clippingPolygonAccelerationGrid: function () { + return clippingPolygon.gridTexture; + }, + + u_clippingPolygonMeshPositions: function () { + return clippingPolygon.meshPositionsTexture; + }, + + u_clippingPolygonOverlappingTriangleIndices: function () { + return clippingPolygon.overlappingTriangleIndicesTexture; + }, + + u_clippingPolygonAccelerationGridPixelDimensions: function () { + return clippingPolygon.gridPixelDimensions; + }, + + u_clippingPolygonOverlappingTrianglePixelIndicesDimensions: function () { + return clippingPolygon.overlappingTrianglePixelIndicesDimensions; + }, + + u_clippingPolygonMeshPositionPixelDimensions: function () { + return clippingPolygon.meshPositionPixelDimensions; + }, + + u_clippingPolygonEyeToWorldToENU: function () { + var eyeToWorld = context.uniformState.inverseView3D; + var eyeToWorldToENU = Matrix4.multiply( + clippingPolygon.worldToENU, + eyeToWorld, + scratchClippingPolygonEyeToWorldMatrix + ); + + return eyeToWorldToENU; + }, + + u_clippingPolygonMinimumZ: function () { + return clippingPolygon.minimumZ; + }, + }); + } + // Allow callback to modify the uniformMap if (defined(model._uniformMapLoaded)) { uniformMap = model._uniformMapLoaded(uniformMap, programId, runtimeNode); @@ -4783,6 +4870,26 @@ function modifyShaderForClippingPlanes( return shader; } +function modifyShaderForClippingPolygon(shader, clippingPolygon, context) { + shader = ShaderSource.replaceMain(shader, "gltf_clip_polygon_main"); + var clippingFunc = getPolygonClippingFunction(clippingPolygon.union); + // TODO: We should avoid clip space entirely to avoid precision issues + // with the clipspace -> ec -> eyespace conversion. See Globe.VS + // and how it forwards EC coordinates to the fragment shader if + // clipping polygons for the globe are enabled. + + var newMain = + "void main() {\n" + + "vec4 positionEC = czm_windowToEyeCoordinates(gl_FragCoord);\n" + + "positionEC /= positionEC.w;\n" + + "vec4 positionENU = u_clippingPolygonEyeToWorldToENU * vec4(positionEC.xyz, 1.0);\n" + + "clippingPolygon(positionENU.xyz);\n" + + "gltf_clip_polygon_main();\n" + + "}\n"; + + return shader + "#define ENABLE_CLIPPING_POLYGON\n" + clippingFunc + newMain; +} + function updateSilhouette(model, frameState, force) { // Generate silhouette commands when the silhouette size is greater than 0.0 and the alpha is greater than 0.0 // There are two silhouette commands: @@ -4818,6 +4925,13 @@ function updateClippingPlanes(model, frameState) { } } +function updateClippingPolygon(model, frameState) { + var clippingPolygon = model._clippingPolygon; + if (defined(clippingPolygon)) { + clippingPolygon.update(frameState); + } +} + var scratchBoundingSphere = new BoundingSphere(); function scaleInPixels(positionWC, radius, frameState) { @@ -5469,6 +5583,7 @@ Model.prototype.update = function (frameState) { updateShowBoundingVolume(this); updateShadows(this); updateClippingPlanes(this, frameState); + updateClippingPolygon(this, frameState); // Regenerate shaders if ClippingPlaneCollection state changed or it was removed var clippingPlanes = this._clippingPlanes; @@ -5500,6 +5615,13 @@ Model.prototype.update = function (frameState) { currentClippingPlanesState = clippingPlanes.clippingPlanesState; } + // TODO: inelegant + var clippingPolygon = this._clippingPolygon; + if (defined(clippingPolygon)) { + // TODO: This should be behind a dirty flag. + shouldRegenerateShaders = true; + } + var shouldRegenerateShaders = this._shouldRegenerateShaders; shouldRegenerateShaders = shouldRegenerateShaders || diff --git a/Source/Scene/PolygonClippingAccelerationGrid.js b/Source/Scene/PolygonClippingAccelerationGrid.js new file mode 100644 index 000000000000..86dbdb2bd4e0 --- /dev/null +++ b/Source/Scene/PolygonClippingAccelerationGrid.js @@ -0,0 +1,520 @@ +import defaultValue from "../Core/defaultValue.js"; +import Cartesian2 from "../Core/Cartesian2.js"; +import DeveloperError from "../Core/DeveloperError.js"; + +// https://stackoverflow.com/a/2049593 +function sign(ax, ay, bx, by, cx, cy) { + return (ax - cx) * (by - cy) - (bx - cx) * (ay - cy); +} + +/** + * Detect if a point is inside of a triangle. + * @param {Number} px x location of point to test + * @param {Number} py y location of point to test + * @param {Number} ax x position of first triangle vertex + * @param {Number} ay y position of first triangle vertex + * @param {Number} bx x position of second triangle vertex + * @param {Number} by y position of second triangle vertex + * @param {Number} cx x position of third triangle vertex + * @param {Number} cy y position of third triangle vertex + */ + +function pointInTriangle(px, py, ax, ay, bx, by, cx, cy) { + var d1 = sign(px, py, ax, ay, bx, by); + var d2 = sign(px, py, bx, by, cx, cy); + var d3 = sign(px, py, cx, cy, ax, ay); + var isNegative = d1 < 0 || d2 < 0 || d3 < 0; + var isPositive = d1 > 0 || d2 > 0 || d3 > 0; + return !(isNegative && isPositive); +} + +function pointInsideRect(rect, px, py) { + var leftWall = px <= rect.topRight.x; + var rightWall = px >= rect.topLeft.x; + var topWall = py <= rect.topLeft.y; + var btmWall = py >= rect.btmRight.y; + return leftWall && rightWall && topWall && btmWall; +} + +// https://stackoverflow.com/a/58657254 +export function lineIntersection(s1x, s1y, t1x, t1y, s2x, s2y, t2x, t2y) { + var dX = t1x - s1x; + var dY = t1y - s1y; + + var determinant = dX * (t2y - s2y) - (t2x - s2x) * dY; + if (determinant === 0) return null; // parallel lines + + var lambda = + ((t2y - s2y) * (t2x - s1x) + (s2x - t2x) * (t2y - s1y)) / determinant; + + var gamma = ((s1y - t1y) * (t2x - s1x) + dX * (t2y - s1y)) / determinant; + + // check if there is an lineIntersection + if (!(0 <= lambda && lambda <= 1) || !(0 <= gamma && gamma <= 1)) return null; + + return { + x: s1x + lambda * dX, + y: s1y + lambda * dY, + }; +} + +function doesTriangleIntersectRect(rect, ax, ay, bx, by, cx, cy) { + // check if the points are inside the rectangle + var leftWall = ax <= rect.topRight.x; + var rightWall = ax >= rect.topLeft.x; + var topWall = ay <= rect.topLeft.y; + var btmWall = ay >= rect.btmRight.y; + + if (leftWall && rightWall && topWall && btmWall) { + return true; + } + + leftWall = bx <= rect.topRight.x; + rightWall = bx >= rect.topLeft.x; + topWall = by <= rect.topLeft.y; + btmWall = by >= rect.btmRight.y; + + if (leftWall && rightWall && topWall && btmWall) { + return true; + } + + leftWall = cx <= rect.topRight.x; + rightWall = cx >= rect.topLeft.x; + topWall = cy <= rect.topLeft.y; + btmWall = cy >= rect.btmRight.y; + + if (leftWall && rightWall && topWall && btmWall) { + return true; + } + + if ( + lineIntersection( + rect.topLeft.x, + rect.topLeft.y, + rect.topRight.x, + rect.topRight.y, + ax, + ay, + bx, + by + ) !== null + ) + return true; + if ( + lineIntersection( + rect.btmLeft.x, + rect.btmLeft.y, + rect.btmRight.x, + rect.btmRight.y, + ax, + ay, + bx, + by + ) !== null + ) + return true; + if ( + lineIntersection( + rect.topLeft.x, + rect.topLeft.y, + rect.btmLeft.x, + rect.btmRight.y, + ax, + ay, + bx, + by + ) !== null + ) + return true; + if ( + lineIntersection( + rect.topRight.x, + rect.topRight.y, + rect.btmRight.x, + rect.btmRight.y, + ax, + ay, + bx, + by + ) !== null + ) + return true; + + if ( + lineIntersection( + rect.topLeft.x, + rect.topLeft.y, + rect.topRight.x, + rect.topRight.y, + bx, + by, + cx, + cy + ) !== null + ) + return true; + if ( + lineIntersection( + rect.btmLeft.x, + rect.btmLeft.y, + rect.btmRight.x, + rect.btmRight.y, + bx, + by, + cx, + cy + ) !== null + ) + return true; + if ( + lineIntersection( + rect.topLeft.x, + rect.topLeft.y, + rect.btmLeft.x, + rect.btmRight.y, + bx, + by, + cx, + cy + ) !== null + ) + return true; + if ( + lineIntersection( + rect.topRight.x, + rect.topRight.y, + rect.btmRight.x, + rect.btmRight.y, + bx, + by, + cx, + cy + ) !== null + ) + return true; + + if ( + lineIntersection( + rect.topLeft.x, + rect.topLeft.y, + rect.topRight.x, + rect.topRight.y, + ax, + ay, + cx, + cy + ) !== null + ) + return true; + if ( + lineIntersection( + rect.btmLeft.x, + rect.btmLeft.y, + rect.btmRight.x, + rect.btmRight.y, + ax, + ay, + cx, + cy + ) !== null + ) + return true; + if ( + lineIntersection( + rect.topLeft.x, + rect.topLeft.y, + rect.btmLeft.x, + rect.btmRight.y, + ax, + ay, + cx, + cy + ) !== null + ) + return true; + if ( + lineIntersection( + rect.topRight.x, + rect.topRight.y, + rect.btmRight.x, + rect.btmRight.y, + ax, + ay, + cx, + cy + ) !== null + ) + return true; + + return false; +} + +function doesTriangleOverlapRect(rect, ax, ay, bx, by, cx, cy) { + var r = rect; + + if (!pointInTriangle(r.topLeft.x, r.topLeft.y, ax, ay, bx, by, cx, cy)) { + return false; + } + + if (!pointInTriangle(r.topRight.x, r.topRight.y, ax, ay, bx, by, cx, cy)) { + return false; + } + + if (!pointInTriangle(r.btmRight.x, r.btmRight.y, ax, ay, bx, by, cx, cy)) { + return false; + } + + return pointInTriangle(r.btmLeft.x, r.btmLeft.y, ax, ay, bx, by, cx, cy); +} + +/** + * Create a 2D bounding box from position / index data. + * + * @param {Object} options Object with the following properties: + * @param {Array.} options.positions An array of position data + * representing the clipping mesh. The bounding box is always 2D in nature, + * so use `3`, `0` and `1` to choose which dimensions + * should be looked at to varruct the bounding box. + * @param {Array.} options.indices Triangle index data corresponding + * to the positions data. + * + * @private + */ + +function ClippingBoundingBox2D(options) { + var positions = options.positions; + var indices = options.indices; + + var minX = Infinity; + var minY = Infinity; + var maxX = -Infinity; + var maxY = -Infinity; + + var i; + for (i = 0; i < indices.length; i += 3) { + var ax = positions[indices[i] * 3]; + var ay = positions[indices[i] * 3 + 1]; + var bx = positions[indices[i + 1] * 3]; + var by = positions[indices[i + 1] * 3 + 1]; + var cx = positions[indices[i + 2] * 3]; + var cy = positions[indices[i + 2] * 3 + 1]; + + minX = Math.min(ax, minX); + minX = Math.min(bx, minX); + minX = Math.min(cx, minX); + minY = Math.min(ay, minY); + minY = Math.min(by, minY); + minY = Math.min(cy, minY); + + maxX = Math.max(ax, maxX); + maxX = Math.max(bx, maxX); + maxX = Math.max(cx, maxX); + maxY = Math.max(ay, maxY); + maxY = Math.max(by, maxY); + maxY = Math.max(cy, maxY); + } + + this.topLeft = { x: minX, y: maxY }; + this.topRight = { x: maxX, y: maxY }; + this.btmRight = { x: maxX, y: minY }; + this.btmLeft = { x: minX, y: minY }; + this.width = this.topRight.x - this.topLeft.x; + this.height = this.topRight.y - this.btmRight.y; +} + +ClippingBoundingBox2D.prototype.toClockwiseCartesian2Pairs = function () { + return [ + new Cartesian2(this.topLeft.x, this.topLeft.y), + new Cartesian2(this.topRight.x, this.topRight.y), + new Cartesian2(this.btmRight.x, this.btmRight.y), + new Cartesian2(this.btmLeft.x, this.btmLeft.y), + ]; +}; + +PolygonClippingAccelerationGrid.CellOcclusion = { + None: 1, + Partial: 2, + Total: 3, +}; + +/** + * Creates two acceleration RGB Float32Array data textures that speed up + * point in polygon queries against a given 2D mesh. The first data texture, + * 'grid', is a 2D array with (splits + 1)^2 partitions, where each cell + * records the occlusion status of that cell with respect to all of the + * triangles in the inputted mesh. If a cell is completely occluded by at least + * one triangle in the mesh (CellOcclusion.Total) or a cell is not occluded + * by any triangles in the mesh (CellOcclusion.None) then the respective + * constant is written to grids[i * 3] where `i` is the 1D index + * of the 2D location in the grid array (obtained via (row * this.numCols) + col). + * + * If the cell is partially occluded by at least one triangle, then the constant + * CellOcclusion.Partial is written to grids[i * 3], and a list of each triangle + * that partially intersects this cell is inserted into 'overlappingTriangleIndices', + * and the inclusive / exclusive index of where to read those values is stored + * stored in grids[(i * 3) + 1] and grids[(i * 3) + 2] respectively. + * + * @alias PolygonClippingAccelerationGrid + * @constructor + * + * @param {Object} options Object with the following properties: + * @param {Array.} options.positions An array of position data + * representing the clipping mesh. Note that this function only supports + * generating 2D clipping data, so one dimension of data will always be + * thrown away. + * @param {Array.} options.indices Triangle index data corresponding + * to the positions data. + * @param {Number} [options.splits-32] The number of cells to place in the + * polygon clipping acceleration grid. Will generate (splits + 1)^2 cells. + * @private + */ +function PolygonClippingAccelerationGrid(options) { + this.overlappingTriangleIndices = undefined; + var positions = defaultValue(options.positions, []); + var indices = defaultValue(options.indices, []); + var splits = defaultValue(options.splits, 32); + + if (positions.length < 3) { + throw new DeveloperError( + "PolygonClippingAccelerationGrid requires at least 3 positions, " + + "(Found " + + options.positions.length + + " instead)." + ); + } + + if (indices.length < 3 || indices.length % 3 !== 0) { + throw new DeveloperError( + "PolygonClippingAccelerationGrid requires indices is non-zero" + + " and a multiple of three" + ); + } + + var bbox = new ClippingBoundingBox2D(options); + + // each cell is 3 numbers (occlusion status, start index, end index) + var cellNumElements = 3; + this.numCols = splits + 1; + this.numRows = this.numCols; // always square for now + var numCells = this.numCols * this.numRows; + + var allOverlappingTriangles = []; + this.grid = new Float32Array(numCells * cellNumElements); + this.cellWidth = bbox.width / this.numCols; + this.cellHeight = bbox.height / this.numRows; + this.boundingBox = bbox; + this.cellNumElements = cellNumElements; + + var row; + var col; + + for (row = 0; row < this.numRows; ++row) { + for (col = 0; col < this.numCols; ++col) { + var cellTopLeft = { + x: bbox.topLeft.x + col * this.cellWidth, + y: bbox.topRight.y - this.cellHeight * row, + }; + + var cellTopRight = { + x: bbox.topLeft.x + (col + 1) * this.cellWidth, + y: cellTopLeft.y, + }; + + var cellBtmRight = { + x: cellTopRight.x, + y: bbox.topRight.y - this.cellHeight * (row + 1), + }; + + var cellBtmLeft = { + x: cellTopLeft.x, + y: cellBtmRight.y, + }; + + var cell = { + topLeft: cellTopLeft, + topRight: cellTopRight, + btmRight: cellBtmRight, + btmLeft: cellBtmLeft, + }; + + var cellIsIndeterminate = false; + var cellIsCompletelyOccluded = false; + var partiallyOverlappingTriangleIndices = []; + + for (let i = 0; i < indices.length; i += 3) { + var ax = positions[indices[i] * 3]; + var ay = positions[indices[i] * 3 + 1]; + var bx = positions[indices[i + 1] * 3]; + var by = positions[indices[i + 1] * 3 + 1]; + var cx = positions[indices[i + 2] * 3]; + var cy = positions[indices[i + 2] * 3 + 1]; + + // if the edge of this triangle intersects with the cell + // then the cell is indeterminate + if (doesTriangleIntersectRect(cell, ax, ay, bx, by, cx, cy)) { + cellIsIndeterminate = true; + var i0 = indices[i]; + var i1 = indices[i + 1]; + var i2 = indices[i + 2]; + partiallyOverlappingTriangleIndices.push(i0, i1, i2); + } + + if (doesTriangleOverlapRect(cell, ax, ay, bx, by, cx, cy)) { + cellIsCompletelyOccluded = true; + cellIsIndeterminate = false; + partiallyOverlappingTriangleIndices = []; + break; + } + } + + var cellIndex = (row * this.numCols + col) * cellNumElements; + + // if the cell is indeterminate we record all the overlapping + if (cellIsIndeterminate) { + this.grid[cellIndex] = + PolygonClippingAccelerationGrid.CellOcclusion.Partial; + var startIndex = allOverlappingTriangles.length; + var endIndex = startIndex + partiallyOverlappingTriangleIndices.length; + this.grid[cellIndex + 1] = startIndex; + this.grid[cellIndex + 2] = endIndex; + allOverlappingTriangles = allOverlappingTriangles.concat( + partiallyOverlappingTriangleIndices + ); + } else if (cellIsCompletelyOccluded) { + this.grid[cellIndex] = + PolygonClippingAccelerationGrid.CellOcclusion.Total; + } else { + this.grid[cellIndex] = + PolygonClippingAccelerationGrid.CellOcclusion.None; + } + } + } + + this.overlappingTriangleIndices = Float32Array.from(allOverlappingTriangles); +} + +PolygonClippingAccelerationGrid.prototype.getCellFromWorldPosition = function ( + px, + py +) { + if (!pointInsideRect(this.boundingBox, px, py)) { + return null; + } + + var screenX = px + this.boundingBox.width / 2.0; + var screenY = -py + this.boundingBox.height / 2.0; + var row = Math.floor(screenY / this.cellHeight); + var col = Math.floor(screenX / this.cellWidth); + var gridIndex = (row * this.numCols + col) * this.cellNumElements; + var cell = this.grid[gridIndex]; + + return { + status: cell, + startIndex: this.grid[gridIndex + 1], + endIndex: this.grid[gridIndex + 2], + row: row, + col: col, + }; +}; + +export default PolygonClippingAccelerationGrid; diff --git a/Source/Scene/getPolygonClippingFunction.js b/Source/Scene/getPolygonClippingFunction.js new file mode 100644 index 000000000000..0b19b84929dc --- /dev/null +++ b/Source/Scene/getPolygonClippingFunction.js @@ -0,0 +1,384 @@ +/** + * Gets the GLSL functions needed to retrieve clipping polygons + * ClippingPolygon's textures. + * + * @returns {String} A string containing GLSL functions for retrieving clipping planes. + * @private + */ + +function getClippingPolygonFunction(union) { + return ` + uniform vec3 u_clippingPolygonBoundingBox[4]; + uniform vec2 u_clippingPolygonCellDimensions; + uniform sampler2D u_clippingPolygonAccelerationGrid; + uniform sampler2D u_clippingPolygonMeshPositions; + uniform sampler2D u_clippingPolygonOverlappingTriangleIndices; + uniform vec2 u_clippingPolygonAccelerationGridPixelDimensions; + uniform vec2 u_clippingPolygonOverlappingTrianglePixelIndicesDimensions; + uniform vec2 u_clippingPolygonMeshPositionPixelDimensions; + uniform mat4 u_clippingPolygonEyeToWorldToENU; + uniform float u_clippingPolygonMinimumZ; + + #define CLIPPING_POLYGON_BBOX_TOP_LEFT u_clippingPolygonBoundingBox[0] + #define CLIPPING_POLYGON_BBOX_TOP_RIGHT u_clippingPolygonBoundingBox[1] + #define CLIPPING_POLYGON_BBOX_BTM_RIGHT u_clippingPolygonBoundingBox[2] + #define CLIPPING_POLYGON_BBOX_BTM_LEFT u_clippingPolygonBoundingBox[3] + #define CLIPPING_POLYGON_BBOX_WIDTH (CLIPPING_POLYGON_BBOX_TOP_RIGHT.x - CLIPPING_POLYGON_BBOX_TOP_LEFT.x) + #define CLIPPING_POLYGON_BBOX_HEIGHT (CLIPPING_POLYGON_BBOX_TOP_RIGHT.y - CLIPPING_POLYGON_BBOX_BTM_RIGHT.y) + #define CLIPPING_POLYGON_NO_OCCLUSION 1.0 + #define CLIPPING_POLYGON_PARTIAL_OCCLUSION 2.0 + #define CLIPPING_POLYGON_TOTAL_OCCLUSION 3.0 + // if the iteration gets anywhere near this number expect 0fps + #define CLIPPING_MAX_ITERATION 8388608 + ${union ? "#define CLIPPING_POLYGON_UNION" : ""} + float scale(float number, float oldMin, float oldMax, float newMin, float newMax) + { + return (((newMax - newMin) * (number - oldMin)) / (oldMax - oldMin)) + newMin; + } + + float sign (vec2 p1, vec2 p2, vec2 p3) + { + return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); + } + + // https://stackoverflow.com/a/2049593 + bool pointInTriangle(vec2 p, vec2 v1, vec2 v2, vec2 v3) + { + float d1, d2, d3; + bool has_neg, has_pos; + + d1 = sign(p, v1, v2); + d2 = sign(p, v2, v3); + d3 = sign(p, v3, v1); + + has_neg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0); + has_pos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0); + + return !(has_neg && has_pos); + } + + float modI(float a,float b) { + float m = a - floor((a+0.5) / b) * b; + return floor(m + 0.5); + } + + bool isWorldPositionInsideAnyTriangle(vec2 worldPos, float startIndex, float endIndex) + { + float overlappingTriangleIndex = startIndex; + int numTrianglesToCheck = int((endIndex - startIndex) / 3.0); + + for (int k = 0; k < CLIPPING_MAX_ITERATION; k++) + { + if (k >= numTrianglesToCheck) + { + break; + } + + float oneDPixelIndex = overlappingTriangleIndex / 3.0; + + // convert 1D pixel coordinates into 2D pixel coordinates + float overlapPixelX = + (modI(oneDPixelIndex, u_clippingPolygonOverlappingTrianglePixelIndicesDimensions.x) + 0.5) / + u_clippingPolygonOverlappingTrianglePixelIndicesDimensions.x; + + float overlapPixelY = + (1.0 - (floor(oneDPixelIndex / u_clippingPolygonOverlappingTrianglePixelIndicesDimensions.y) + 0.5)) / + u_clippingPolygonOverlappingTrianglePixelIndicesDimensions.y; + + // grab the relevant verticies for the given triangle in question. + vec4 overlapIndices = texture2D(u_clippingPolygonOverlappingTriangleIndices, vec2(overlapPixelX, overlapPixelY)); + + // convert each mesh index from a 1D index into a 2D pixel coordinate into u_clippingPolygonMeshPositions + vec2 v0PixelIndex = vec2( + (modI(overlapIndices.x, u_clippingPolygonMeshPositionPixelDimensions.x) + 0.5) / + u_clippingPolygonMeshPositionPixelDimensions.x, + (1.0 - (floor(overlapIndices.x / u_clippingPolygonMeshPositionPixelDimensions.y) + 0.5)) / + u_clippingPolygonMeshPositionPixelDimensions.y + ); + + vec2 v1PixelIndex = vec2( + (modI(overlapIndices.y, u_clippingPolygonMeshPositionPixelDimensions.x) + 0.5) / + u_clippingPolygonMeshPositionPixelDimensions.x, + (1.0 - (floor(overlapIndices.y / u_clippingPolygonMeshPositionPixelDimensions.y) + 0.5)) / + u_clippingPolygonMeshPositionPixelDimensions.y + ); + + vec2 v2PixelIndex = vec2( + (modI(overlapIndices.z, u_clippingPolygonMeshPositionPixelDimensions.x) + 0.5) / + u_clippingPolygonMeshPositionPixelDimensions.x, + (1.0 - (floor(overlapIndices.z / u_clippingPolygonMeshPositionPixelDimensions.y) + 0.5)) / + u_clippingPolygonMeshPositionPixelDimensions.y + ); + + vec2 v0 = texture2D(u_clippingPolygonMeshPositions, v0PixelIndex).xy; + vec2 v1 = texture2D(u_clippingPolygonMeshPositions, v1PixelIndex).xy; + vec2 v2 = texture2D(u_clippingPolygonMeshPositions, v2PixelIndex).xy; + + if (pointInTriangle(worldPos, v0, v1, v2)) + { + return true; + } + + overlappingTriangleIndex += 3.0; + } + + return false; + } + + bool pointInsideBoundingBox(vec2 p) + { + vec3 topLeft = CLIPPING_POLYGON_BBOX_TOP_LEFT; + vec3 topRight = CLIPPING_POLYGON_BBOX_TOP_RIGHT; + vec3 btmRight = CLIPPING_POLYGON_BBOX_BTM_RIGHT; + vec3 btmLeft = CLIPPING_POLYGON_BBOX_BTM_LEFT; + bool rightWall = p.x <= topRight.x; + bool leftWall = p.x >= topLeft.x; + bool topWall = p.y <= topLeft.y; + bool btmWall = p.y >= btmLeft.y; + return leftWall && rightWall && topWall && btmWall; + } + + void clippingPolygon(vec3 worldPos) + { + if (worldPos.z < u_clippingPolygonMinimumZ || !pointInsideBoundingBox(worldPos.xy)) + { + #ifndef CLIPPING_POLYGON_UNION + return; + #else + discard; + #endif + } + + float screenX = scale(worldPos.x, CLIPPING_POLYGON_BBOX_TOP_LEFT.x, CLIPPING_POLYGON_BBOX_TOP_RIGHT.x, 0.0, CLIPPING_POLYGON_BBOX_WIDTH); + float screenY = scale(worldPos.y, CLIPPING_POLYGON_BBOX_BTM_RIGHT.y, CLIPPING_POLYGON_BBOX_TOP_RIGHT.y, 0.0, CLIPPING_POLYGON_BBOX_HEIGHT); + float row = floor(screenY / u_clippingPolygonCellDimensions.y); + float col = floor(screenX / u_clippingPolygonCellDimensions.x); + + float gridPixelXIndex = (col + 0.5) / u_clippingPolygonAccelerationGridPixelDimensions.x; + float gridPixelYIndex = (row + 0.5) / u_clippingPolygonAccelerationGridPixelDimensions.y; + + vec3 gridCell = texture2D(u_clippingPolygonAccelerationGrid, vec2(gridPixelXIndex, gridPixelYIndex)).xyz; + + if (gridCell.r == CLIPPING_POLYGON_NO_OCCLUSION) + { + #ifdef CLIPPING_POLYGON_UNION + discard; + #else + return; + #endif + } + + if (gridCell.r == CLIPPING_POLYGON_TOTAL_OCCLUSION) + { + #ifdef CLIPPING_POLYGON_UNION + return; + #else + discard; + #endif + } + + if (isWorldPositionInsideAnyTriangle(worldPos.xy, gridCell.g, gridCell.b)) + { + #ifdef CLIPPING_POLYGON_UNION + return; + #else + discard; + #endif + } + + #ifdef CLIPPING_POLYGON_UNION + discard; + #endif + } + `; +} + +/* +function getClippingPolygonFunction(union) { + return "" + + " uniform vec3 u_clippingPolygonBoundingBox[4];\n" + + " uniform vec2 u_clippingPolygonCellDimensions;\n" + + " uniform sampler2D u_clippingPolygonAccelerationGrid;\n" + + " uniform sampler2D u_clippingPolygonMeshPositions;\n" + + " uniform sampler2D u_clippingPolygonOverlappingTriangleIndices;\n" + + " uniform vec2 u_clippingPolygonAccelerationGridPixelDimensions;\n" + + " uniform vec2 u_clippingPolygonOverlappingTrianglePixelIndicesDimensions;\n" + + " uniform vec2 u_clippingPolygonMeshPositionPixelDimensions;\n" + + " uniform mat4 u_clippingPolygonEyeToWorldToENU;\n" + + " uniform float u_clippingPolygonMinimumZ;\n" + + "\n" + + " #define CLIPPING_POLYGON_BBOX_TOP_LEFT u_clippingPolygonBoundingBox[0]\n" + + " #define CLIPPING_POLYGON_BBOX_TOP_RIGHT u_clippingPolygonBoundingBox[1]\n" + + " #define CLIPPING_POLYGON_BBOX_BTM_RIGHT u_clippingPolygonBoundingBox[2]\n" + + " #define CLIPPING_POLYGON_BBOX_BTM_LEFT u_clippingPolygonBoundingBox[3]\n" + + " #define CLIPPING_POLYGON_BBOX_WIDTH (CLIPPING_POLYGON_BBOX_TOP_RIGHT.x - CLIPPING_POLYGON_BBOX_TOP_LEFT.x)\n" + + " #define CLIPPING_POLYGON_BBOX_HEIGHT (CLIPPING_POLYGON_BBOX_TOP_RIGHT.y - CLIPPING_POLYGON_BBOX_BTM_RIGHT.y)\n" + + " #define CLIPPING_POLYGON_NO_OCCLUSION 1.0\n" + + " #define CLIPPING_POLYGON_PARTIAL_OCCLUSION 2.0\n" + + " #define CLIPPING_POLYGON_TOTAL_OCCLUSION 3.0\n" + + " // if the iteration gets anywhere near this number expect 0fps\n" + + " #define CLIPPING_MAX_ITERATION 8388608\n" + + (union ? "#define CLIPPING_POLYGON_UNION\n" : "\n") + + " float scale(float number, float oldMin, float oldMax, float newMin, float newMax)\n" + + " {\n" + + " return (((newMax - newMin) * (number - oldMin)) / (oldMax - oldMin)) + newMin;\n" + + " }\n" + + "\n" + + " float sign (vec2 p1, vec2 p2, vec2 p3)\n" + + " {\n" + + " return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);\n" + + " }\n" + + "\n" + + " // https://stackoverflow.com/a/2049593\n" + + " bool pointInTriangle(vec2 p, vec2 v1, vec2 v2, vec2 v3)\n" + + " {\n" + + " float d1, d2, d3;\n" + + " bool has_neg, has_pos;\n" + + "\n" + + " d1 = sign(p, v1, v2);\n" + + " d2 = sign(p, v2, v3);\n" + + " d3 = sign(p, v3, v1);\n" + + "\n" + + " has_neg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0);\n" + + " has_pos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0);\n" + + "\n" + + " return !(has_neg && has_pos);\n" + + " }\n" + + "\n" + + " float modI(float a,float b) {\n" + + " float m = a - floor((a+0.5) / b) * b;\n" + + " return floor(m + 0.5);\n" + + " }\n" + + "\n" + + " bool isWorldPositionInsideAnyTriangle(vec2 worldPos, float startIndex, float endIndex)\n" + + " {\n" + + " float overlappingTriangleIndex = startIndex;\n" + + " int numTrianglesToCheck = int((endIndex - startIndex) / 3.0);\n" + + "\n" + + " for (int k = 0; k < CLIPPING_MAX_ITERATION; k++)\n" + + " {\n" + + " if (k >= numTrianglesToCheck)\n" + + " {\n" + + " break;\n" + + " }\n" + + "\n" + + " float oneDPixelIndex = overlappingTriangleIndex / 3.0;\n" + + "\n" + + " // convert 1D pixel coordinates into 2D pixel coordinates\n" + + " float overlapPixelX =\n" + + " (modI(oneDPixelIndex, u_clippingPolygonOverlappingTrianglePixelIndicesDimensions.x) + 0.5) /\n" + + " u_clippingPolygonOverlappingTrianglePixelIndicesDimensions.x;\n" + + "\n" + + " float overlapPixelY =\n" + + " (1.0 - (floor(oneDPixelIndex / u_clippingPolygonOverlappingTrianglePixelIndicesDimensions.y) + 0.5)) /\n" + + " u_clippingPolygonOverlappingTrianglePixelIndicesDimensions.y;\n" + + "\n" + + " // grab the relevant verticies for the given triangle in question.\n" + + " vec4 overlapIndices = texture2D(u_clippingPolygonOverlappingTriangleIndices, vec2(overlapPixelX, overlapPixelY));\n" + + "\n" + + " // convert each mesh index from a 1D index into a 2D pixel coordinate into u_clippingPolygonMeshPositions\n" + + " vec2 v0PixelIndex = vec2(\n" + + " (modI(overlapIndices.x, u_clippingPolygonMeshPositionPixelDimensions.x) + 0.5) /\n" + + " u_clippingPolygonMeshPositionPixelDimensions.x,\n" + + " (1.0 - (floor(overlapIndices.x / u_clippingPolygonMeshPositionPixelDimensions.y) + 0.5)) /\n" + + " u_clippingPolygonMeshPositionPixelDimensions.y\n" + + " );\n" + + "\n" + + " vec2 v1PixelIndex = vec2(\n" + + " (modI(overlapIndices.y, u_clippingPolygonMeshPositionPixelDimensions.x) + 0.5) /\n" + + " u_clippingPolygonMeshPositionPixelDimensions.x,\n" + + " (1.0 - (floor(overlapIndices.y / u_clippingPolygonMeshPositionPixelDimensions.y) + 0.5)) /\n" + + " u_clippingPolygonMeshPositionPixelDimensions.y\n" + + " );\n" + + "\n" + + " vec2 v2PixelIndex = vec2(\n" + + " (modI(overlapIndices.z, u_clippingPolygonMeshPositionPixelDimensions.x) + 0.5) /\n" + + " u_clippingPolygonMeshPositionPixelDimensions.x,\n" + + " (1.0 - (floor(overlapIndices.z / u_clippingPolygonMeshPositionPixelDimensions.y) + 0.5)) /\n" + + " u_clippingPolygonMeshPositionPixelDimensions.y\n" + + " );\n" + + "\n" + + " vec2 v0 = texture2D(u_clippingPolygonMeshPositions, v0PixelIndex).xy;\n" + + " vec2 v1 = texture2D(u_clippingPolygonMeshPositions, v1PixelIndex).xy;\n" + + " vec2 v2 = texture2D(u_clippingPolygonMeshPositions, v2PixelIndex).xy;\n" + + "\n" + + " if (pointInTriangle(worldPos, v0, v1, v2))\n" + + " {\n" + + " return true;\n" + + " }\n" + + "\n" + + " overlappingTriangleIndex += 3.0;\n" + + " }\n" + + "\n" + + " return false;\n" + + " }\n" + + "\n" + + " bool pointInsideBoundingBox(vec2 p)\n" + + " {\n" + + " vec3 topLeft = CLIPPING_POLYGON_BBOX_TOP_LEFT;\n" + + " vec3 topRight = CLIPPING_POLYGON_BBOX_TOP_RIGHT;\n" + + " vec3 btmRight = CLIPPING_POLYGON_BBOX_BTM_RIGHT;\n" + + " vec3 btmLeft = CLIPPING_POLYGON_BBOX_BTM_LEFT;\n" + + " bool rightWall = p.x <= topRight.x;\n" + + " bool leftWall = p.x >= topLeft.x;\n" + + " bool topWall = p.y <= topLeft.y;\n" + + " bool btmWall = p.y >= btmLeft.y;\n" + + " return leftWall && rightWall && topWall && btmWall;\n" + + " }\n" + + "\n" + + " void clippingPolygon(vec3 worldPos)\n" + + " {\n" + + " if (worldPos.z < u_clippingPolygonMinimumZ || !pointInsideBoundingBox(worldPos.xy))\n" + + " {\n" + + " #ifndef CLIPPING_POLYGON_UNION\n" + + " return;\n" + + " #else\n" + + " discard;\n" + + " #endif\n" + + " }\n" + + "\n" + + " float screenX = scale(worldPos.x, CLIPPING_POLYGON_BBOX_TOP_LEFT.x, CLIPPING_POLYGON_BBOX_TOP_RIGHT.x, 0.0, CLIPPING_POLYGON_BBOX_WIDTH);\n" + + " float screenY = scale(worldPos.y, CLIPPING_POLYGON_BBOX_BTM_RIGHT.y, CLIPPING_POLYGON_BBOX_TOP_RIGHT.y, 0.0, CLIPPING_POLYGON_BBOX_HEIGHT);\n" + + " float row = floor(screenY / u_clippingPolygonCellDimensions.y);\n" + + " float col = floor(screenX / u_clippingPolygonCellDimensions.x);\n" + + "\n" + + " float gridPixelXIndex = (col + 0.5) / u_clippingPolygonAccelerationGridPixelDimensions.x;\n" + + " float gridPixelYIndex = (row + 0.5) / u_clippingPolygonAccelerationGridPixelDimensions.y;\n" + + "\n" + + " vec3 gridCell = texture2D(u_clippingPolygonAccelerationGrid, vec2(gridPixelXIndex, gridPixelYIndex)).xyz;\n" + + "\n" + + " if (gridCell.r == CLIPPING_POLYGON_NO_OCCLUSION)\n" + + " {\n" + + " #ifdef CLIPPING_POLYGON_UNION\n" + + " discard;\n" + + " #else\n" + + " return;\n" + + " #endif\n" + + " }\n" + + "\n" + + " if (gridCell.r == CLIPPING_POLYGON_TOTAL_OCCLUSION)\n" + + " {\n" + + " #ifdef CLIPPING_POLYGON_UNION\n" + + " return;\n" + + " #else\n" + + " discard;\n" + + " #endif\n" + + " }\n" + + "\n" + + " if (isWorldPositionInsideAnyTriangle(worldPos.xy, gridCell.g, gridCell.b))\n" + + " {\n" + + " #ifdef CLIPPING_POLYGON_UNION\n" + + " return;\n" + + " #else\n" + + " discard;\n" + + " #endif\n" + + " }\n" + + "\n" + + " #ifdef CLIPPING_POLYGON_UNION\n" + + " discard;\n" + + " #endif\n" + + " }\n" +} + */ + +export default getClippingPolygonFunction; diff --git a/Source/Shaders/GlobeFS.glsl b/Source/Shaders/GlobeFS.glsl index 10a5c29fc3f4..f20f5063f70f 100644 --- a/Source/Shaders/GlobeFS.glsl +++ b/Source/Shaders/GlobeFS.glsl @@ -77,6 +77,7 @@ uniform mat4 u_clippingPlanesMatrix; uniform vec4 u_clippingPlanesEdgeStyle; #endif + #if defined(FOG) && defined(DYNAMIC_ATMOSPHERE_LIGHTING) && (defined(ENABLE_VERTEX_LIGHTING) || defined(ENABLE_DAYNIGHT_SHADING)) uniform float u_minimumBrightness; #endif @@ -300,6 +301,11 @@ void main() } #endif +#ifdef ENABLE_CLIPPING_POLYGON + vec4 positionENU = u_clippingPolygonEyeToWorldToENU * vec4(v_positionEC.xyz, 1.0); + clippingPolygon(positionENU.xyz); +#endif + #ifdef ENABLE_CLIPPING_PLANES float clipDistance = clip(gl_FragCoord, u_clippingPlanes, u_clippingPlanesMatrix); #endif diff --git a/Source/Shaders/GlobeVS.glsl b/Source/Shaders/GlobeVS.glsl index e233638f8008..7d8b4dc6764e 100644 --- a/Source/Shaders/GlobeVS.glsl +++ b/Source/Shaders/GlobeVS.glsl @@ -157,8 +157,10 @@ void main() vec3 position3DWC = position + u_center3D; gl_Position = getPosition(position, height, textureCoordinates); - v_textureCoordinates = vec3(textureCoordinates, webMercatorT); +#if defined(ENABLE_CLIPPING_POLYGON) + v_positionEC = (u_modifiedModelView * vec4(position, 1.0)).xyz; +#endif #if defined(ENABLE_VERTEX_LIGHTING) || defined(GENERATE_POSITION_AND_NORMAL) || defined(APPLY_MATERIAL) v_positionEC = (u_modifiedModelView * vec4(position, 1.0)).xyz; diff --git a/Source/ThirdParty/simplify3d.js b/Source/ThirdParty/simplify3d.js new file mode 100644 index 000000000000..556d49a50368 --- /dev/null +++ b/Source/ThirdParty/simplify3d.js @@ -0,0 +1,138 @@ +/* + (c) 2013, Vladimir Agafonkin + Simplify.js, a high-performance JS polyline simplification library + mourner.github.io/simplify-js +*/ + +// to suit your point format, run search/replace for '.x', '.y' and '.z'; +// (configurability would draw significant performance overhead) + +// square distance between 2 points +function getSquareDistance(p1, p2) { + + var dx = p1.x - p2.x, + dy = p1.y - p2.y, + dz = p1.z - p2.z; + + return dx * dx + dy * dy + dz * dz; +} + +// square distance from a point to a segment +function getSquareSegmentDistance(p, p1, p2) { + + var x = p1.x, + y = p1.y, + z = p1.z, + + dx = p2.x - x, + dy = p2.y - y, + dz = p2.z - z; + + if (dx !== 0 || dy !== 0 || dz !== 0) { + + var t = ((p.x - x) * dx + (p.y - y) * dy + (p.z - z) * dz) / + (dx * dx + dy * dy + dz * dz); + + if (t > 1) { + x = p2.x; + y = p2.y; + z = p2.z; + + } else if (t > 0) { + x += dx * t; + y += dy * t; + z += dz * t; + } + } + + dx = p.x - x; + dy = p.y - y; + dz = p.z - z; + + return dx * dx + dy * dy + dz * dz; +} +// the rest of the code doesn't care for the point format + +// basic distance-based simplification +function simplifyRadialDistance(points, sqTolerance) { + + var prevPoint = points[0], + newPoints = [prevPoint], + point; + + for (var i = 1, len = points.length; i < len; i++) { + point = points[i]; + + if (getSquareDistance(point, prevPoint) > sqTolerance) { + newPoints.push(point); + prevPoint = point; + } + } + + if (prevPoint !== point) { + newPoints.push(point); + } + + return newPoints; +} + +// simplification using optimized Douglas-Peucker algorithm with recursion elimination +function simplifyDouglasPeucker(points, sqTolerance) { + + var len = points.length, + MarkerArray = typeof Uint8Array !== 'undefined' ? Uint8Array : Array, + markers = new MarkerArray(len), + + first = 0, + last = len - 1, + + stack = [], + newPoints = [], + + i, maxSqDist, sqDist, index; + + markers[first] = markers[last] = 1; + + while (last) { + + maxSqDist = 0; + + for (i = first + 1; i < last; i++) { + sqDist = getSquareSegmentDistance(points[i], points[first], points[last]); + + if (sqDist > maxSqDist) { + index = i; + maxSqDist = sqDist; + } + } + + if (maxSqDist > sqTolerance) { + markers[index] = 1; + stack.push(first, index, index, last); + } + + last = stack.pop(); + first = stack.pop(); + } + + for (i = 0; i < len; i++) { + if (markers[i]) { + newPoints.push(points[i]); + } + } + + return newPoints; +} + +// both algorithms combined for awesome performance +function simplify3d(points, tolerance, highestQuality) { + + var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1; + + points = highestQuality ? points : simplifyRadialDistance(points, sqTolerance); + points = simplifyDouglasPeucker(points, sqTolerance); + + return points; +} + +export default simplify3d; diff --git a/Specs/Scene/PolygonClippingAccelerationGridSpec.js b/Specs/Scene/PolygonClippingAccelerationGridSpec.js new file mode 100644 index 000000000000..5a5cc0bcd7fb --- /dev/null +++ b/Specs/Scene/PolygonClippingAccelerationGridSpec.js @@ -0,0 +1,155 @@ +import PolygonClippingAccelerationGrid from "../../Source/Scene/PolygonClippingAccelerationGrid.js"; + +describe("Scene/PolygonClippingAcceleration", function () { + it("developer error if invalid number of positions or indicies provided", function () { + expect(function () { + var empty_arrays = new PolygonClippingAccelerationGrid({ + positions: [], + indices: [], + splits: 0, + }); + }).toThrowError(); + + expect(function () { + var missing_indices = new PolygonClippingAccelerationGrid({ + positions: [1, 2, 3], + splits: 0, + }); + }).toThrowError(); + + expect(function () { + var missing_positions = new PolygonClippingAccelerationGrid({ + indices: [1, 2, 3], + splits: 0, + }); + }).toThrowError(); + }); + + it("square with four splits is correctly generated", function () { + // prettier-ignore + const positions = [ + -1, 1, 0, // v0-|--v1 row0col0 (0) is PARTIAL + 1, 1, 0, // |0\| 1| row0col1 (1) is TOTAL + 1, -1, 0, // ------- row1col0 (2) is TOTAL + -1, -1, 0 // |2 |\3| row1col1 (3) is PARTIAL + ]; // prettier-ignore // v3-|-v2 + + const indices = [0, 1, 2, 2, 3, 0]; + const accelerator = new PolygonClippingAccelerationGrid({ + positions: positions, + indices: indices, + splits: 1, + numDimensions: 3, + xIndex: 0, + yIndex: 2, + }); + + const grid = accelerator.grid; + const overlappingIndices = accelerator.overlappingTriangleIndices; + + expect(grid.length).toEqual(12); + expect(overlappingIndices.length).toEqual(12); + + // row 0, col 0 + const cell0 = grid[0]; + expect(cell0).toEqual( + PolygonClippingAccelerationGrid.CellOcclusion.Partial + ); + expect(Array.from(overlappingIndices.slice(grid[1], grid[2]))).toEqual( + indices + ); + + // row 0, col 1 + const cell1 = grid[3]; + expect(cell1).toEqual(PolygonClippingAccelerationGrid.CellOcclusion.Total); + expect(grid[4]).toEqual(0); + + // row 1, col 0 + const cell2 = grid[6]; + expect(cell2).toEqual(PolygonClippingAccelerationGrid.CellOcclusion.Total); + + // row 1, col 1 + const cell3 = grid[9]; + expect(cell3).toEqual( + PolygonClippingAccelerationGrid.CellOcclusion.Partial + ); + expect(Array.from(overlappingIndices.slice(grid[10], grid[11]))).toEqual( + indices + ); + }); + + it("zero split with single triangle to be partially occluded", function () { + // prettier-ignore + const positions = [ + 0, 0, 0, + 0, -10, 0, + 10, 0, 0 + ]; + + const indices = [0, 1, 2]; + + const result = new PolygonClippingAccelerationGrid({ + positions: positions, + indices: indices, + splits: 0, + }); + + const grid = result.grid; + expect(grid.length).toEqual(3); + expect(grid[0]).toEqual( + PolygonClippingAccelerationGrid.CellOcclusion.Partial + ); + }); + + it("getCellFromWorldPosition maps world coordinates to cell coordinates correctly", function () { + // prettier-ignore + const positions = [ + 0, 3, 0, + 3, -3, 0, + -3, -3, 0 + ]; + + const indices = [0, 1, 2]; + + const result = new PolygonClippingAccelerationGrid({ + positions: positions, + indices: indices, + splits: 1, + }); + + const topLeft = result.getCellFromWorldPosition(-1.5, 1.5); + expect(topLeft).toBeDefined(); + expect(topLeft.col).toEqual(0); + expect(topLeft.row).toEqual(0); + expect(topLeft.status).toEqual( + PolygonClippingAccelerationGrid.CellOcclusion.Partial + ); + + const btmLeft = result.getCellFromWorldPosition(-1.5, -1.5); + expect(btmLeft).toBeDefined(); + expect(btmLeft.col).toEqual(0); + expect(btmLeft.row).toEqual(1); + expect(btmLeft.status).toEqual( + PolygonClippingAccelerationGrid.CellOcclusion.Partial + ); + }); + + it("getCellFromWorldPosition returns null if location is out of bounds", function () { + // prettier-ignore + const positions = [ + -1, 1, 0, // v0-|--v1 row0col0 (0) is PARTIAL + 1, 1, 0, // |0\| 1| row0col1 (1) is TOTAL + 1, -1, 0, // ------- row1col0 (2) is TOTAL + -1, -1, 0 // |2 |\3| row1col1 (3) is PARTIAL + ]; // prettier-ignore // v3-|-v2 + + const indices = [0, 1, 2, 2, 3, 0]; + const accelerator = new PolygonClippingAccelerationGrid({ + positions: positions, + indices: indices, + splits: 1, + }); + + expect(accelerator.getCellFromWorldPosition(1995, 2020)).toBeNull(); + }); +});