diff --git a/Apps/CesiumViewer/CesiumViewer.js b/Apps/CesiumViewer/CesiumViewer.js index 3237d5ef8be3..ea6b571be395 100644 --- a/Apps/CesiumViewer/CesiumViewer.js +++ b/Apps/CesiumViewer/CesiumViewer.js @@ -1,6 +1,7 @@ -if (window.CESIUM_BASE_URL === undefined) { - window.CESIUM_BASE_URL = "../../Build/CesiumUnminified/"; -} +// eslint-disable-next-line no-undef +window.CESIUM_BASE_URL = window.CESIUM_BASE_URL + ? window.CESIUM_BASE_URL + : "../../Build/CesiumUnminified/"; import { Cartesian3, diff --git a/Apps/CesiumViewer/index.js b/Apps/CesiumViewer/index.js deleted file mode 100644 index f4e3e3d56f26..000000000000 --- a/Apps/CesiumViewer/index.js +++ /dev/null @@ -1 +0,0 @@ -window.CESIUM_BASE_URL = "."; diff --git a/CHANGES.md b/CHANGES.md index 3798045bc86f..06888e445cda 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,8 @@ try { - Fixed issue where passing `children` in the Entity constructor options will override children. [#11101](https://github.com/CesiumGS/cesium/issues/11101) - Fixed error type to be `RequestErrorEvent` in `Resource.retryCallback`. [#11177](https://github.com/CesiumGS/cesium/pull/11177) +- Fixed issue when render `OrthographicFrustum` geometry by `DebugCameraPrimitive`. [#11159](https://github.com/CesiumGS/cesium/issues/11159) +- Fixed ion URL in `RequestScheduler` throttling overrides. [#11193](https://github.com/CesiumGS/cesium/pull/11193) - Fixed `SingleTileImageryProvider` fetching image when `show` is `false` by allowing lazy-loading for `SingleTileImageryProvider` if `tileWidth` and `tileHeight` are provided to the constructor. [#9529](https://github.com/CesiumGS/cesium/issues/9529) - Fixed various race conditions from async operations. [#10909](https://github.com/CesiumGS/cesium/issues/10909) diff --git a/Documentation/CustomShaderGuide/README.md b/Documentation/CustomShaderGuide/README.md index 714af9604308..bec25d80b127 100644 --- a/Documentation/CustomShaderGuide/README.md +++ b/Documentation/CustomShaderGuide/README.md @@ -144,12 +144,12 @@ The user is responsible for assigning a value to this varying in const customShader = new Cesium.CustomShader({ // Varying is declared here varyings: { - v_selectedColor: VaryingType.VEC3, + v_selectedColor: Cesium.VaryingType.VEC4, }, // User assigns the varying in the vertex shader vertexShaderText: ` void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput) { - float positiveX = step(0.0, positionMC.x); + float positiveX = step(0.0, vsOutput.positionMC.x); v_selectedColor = mix( vsInput.attributes.color_0, vsInput.attributes.color_1, @@ -160,7 +160,7 @@ const customShader = new Cesium.CustomShader({ // User uses the varying in the fragment shader fragmentShaderText: ` void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { - material.diffuse = v_selectedColor; + material.diffuse = v_selectedColor.rgb; } `, }); @@ -838,7 +838,7 @@ For `ENUM` type metadata, the statistics struct for that property should contain ## `czm_modelVertexOutput` struct -This struct is built-in, see the [documentation comment](../../../Shaders/Builtin/Structs/modelVertexOutput.glsl). +This struct is built-in, see the [documentation comment](../../packages/engine/Source/Shaders/Builtin/Structs/modelVertexOutput.glsl). This struct contains the output of the custom vertex shader. This includes: @@ -856,7 +856,7 @@ This struct contains the output of the custom vertex shader. This includes: ## `czm_modelMaterial` struct -This struct is a built-in, see the [documentation comment](../../Source/Shaders/Builtin/Structs/modelMaterial.glsl). This is similar to `czm_material` from the old Fabric system, but has slightly different fields as this one supports PBR lighting. +This struct is a built-in, see the [documentation comment](../../packages/engine/Source/Shaders/Builtin/Structs/modelMaterial.glsl). This is similar to `czm_material` from the old Fabric system, but has slightly different fields as this one supports PBR lighting. This struct serves as the basic input/output of the fragment shader pipeline stages. For example: diff --git a/gulpfile.js b/gulpfile.js index ef228912e4f9..51a36176f101 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2230,7 +2230,8 @@ async function buildCesiumViewer() { ".png": "text", }; config.format = "iife"; - config.inject = ["Apps/CesiumViewer/index.js"]; + // Configure Cesium base path to use built + config.define = { CESIUM_BASE_URL: `"."` }; config.external = ["https", "http", "url", "zlib"]; config.outdir = cesiumViewerOutputDirectory; config.outbase = "Apps/CesiumViewer"; @@ -2247,7 +2248,7 @@ async function buildCesiumViewer() { ".gif": "text", ".png": "text", }, - outdir: cesiumViewerOutputDirectory, + outdir: join(cesiumViewerOutputDirectory, "Widgets"), outbase: "packages/widgets/Source/", }); diff --git a/package.json b/package.json index f755bdb9a61f..5122b730fd44 100644 --- a/package.json +++ b/package.json @@ -97,14 +97,14 @@ "karma-jasmine": "^5.1.0", "karma-longest-reporter": "^1.1.0", "karma-safari-launcher": "^1.0.0", - "karma-sourcemap-loader": "^0.3.8", + "karma-sourcemap-loader": "^0.4.0", "karma-spec-reporter": "^0.0.36", "markdownlint-cli": "^0.33.0", "merge-stream": "^2.0.0", "mime": "^3.0.0", "mkdirp": "^2.1.3", "node-fetch": "^3.2.10", - "open": "^8.2.1", + "open": "^9.1.0", "p-limit": "^4.0.0", "prettier": "2.1.2", "prismjs": "^1.28.0", @@ -114,7 +114,7 @@ "rollup-plugin-strip-pragma": "^1.0.0", "stream-to-promise": "^3.0.0", "tsd-jsdoc": "^2.5.0", - "typescript": "^4.9.4", + "typescript": "^5.0.2", "yargs": "^17.0.1" }, "scripts": { @@ -167,4 +167,4 @@ "packages/engine", "packages/widgets" ] -} \ No newline at end of file +} diff --git a/packages/engine/Source/Core/FrustumGeometry.js b/packages/engine/Source/Core/FrustumGeometry.js index bece354b733c..0446378d3261 100644 --- a/packages/engine/Source/Core/FrustumGeometry.js +++ b/packages/engine/Source/Core/FrustumGeometry.js @@ -295,8 +295,8 @@ FrustumGeometry._computeNearFarPlanes = function ( let inverseView; let inverseViewProjection; + const projection = frustum.projectionMatrix; if (frustumType === PERSPECTIVE) { - const projection = frustum.projectionMatrix; const viewProjection = Matrix4.multiply( projection, view, diff --git a/packages/engine/Source/Core/RequestScheduler.js b/packages/engine/Source/Core/RequestScheduler.js index a0e4b63c8e4e..43542ad80cc1 100644 --- a/packages/engine/Source/Core/RequestScheduler.js +++ b/packages/engine/Source/Core/RequestScheduler.js @@ -76,7 +76,7 @@ RequestScheduler.maximumRequestsPerServer = 6; */ RequestScheduler.requestsByServer = { "api.cesium.com:443": 18, - "assets.cesium.com:443": 18, + "assets.ion.cesium.com:443": 18, }; /** diff --git a/packages/engine/Source/Scene/Cesium3DTile.js b/packages/engine/Source/Scene/Cesium3DTile.js index eb056f4d7062..713472c0ee47 100644 --- a/packages/engine/Source/Scene/Cesium3DTile.js +++ b/packages/engine/Source/Scene/Cesium3DTile.js @@ -576,6 +576,22 @@ Object.defineProperties(Cesium3DTile.prototype, { }, }, + /** + * Determines if the tile is visible within the current field of view + * + * @memberof Cesium3DTile.prototype + * + * @type {boolean} + * @readonly + * + * @private + */ + isVisible: { + get: function () { + return this._visible && this._inRequestVolume; + }, + }, + /** * Returns the extras property in the tileset JSON for this tile, which contains application specific metadata. * Returns undefined if extras does not exist. @@ -616,6 +632,27 @@ Object.defineProperties(Cesium3DTile.prototype, { }, }, + /** + * Determines if the tile's content is renderable. false if the + * tile has empty content or if it points to an external tileset or implicit content + * + * @memberof Cesium3DTile.prototype + * + * @type {boolean} + * @readonly + * + * @private + */ + hasRenderableContent: { + get: function () { + return ( + !this.hasEmptyContent && + !this.hasTilesetContent && + !this.hasImplicitContent + ); + }, + }, + /** * Determines if the tile has available content to render. true if the tile's * content is ready or if it has expired content that renders while new content loads; otherwise, @@ -631,10 +668,7 @@ Object.defineProperties(Cesium3DTile.prototype, { contentAvailable: { get: function () { return ( - (this.contentReady && - !this.hasEmptyContent && - !this.hasTilesetContent && - !this.hasImplicitContent) || + (this.contentReady && this.hasRenderableContent) || (defined(this._expiredContent) && !this.contentFailed) ); }, @@ -674,6 +708,22 @@ Object.defineProperties(Cesium3DTile.prototype, { }, }, + /** + * Determines if the tile has renderable content which is unloaded + * + * @memberof Cesium3DTile.prototype + * + * @type {boolean} + * @readonly + * + * @private + */ + hasUnloadedRenderableContent: { + get: function () { + return this.hasRenderableContent && this.contentUnloaded; + }, + }, + /** * Determines if the tile's content is expired. true if tile's * content is expired; otherwise, false. @@ -791,7 +841,7 @@ function isPriorityDeferred(tile, frameState) { // Skip this feature if: non-skipLevelOfDetail and replace refine, if the foveated settings are turned off, if tile is progressive resolution and replace refine and skipLevelOfDetail (will help get rid of ancestor artifacts faster) // Or if the tile is a preload of any kind const replace = tile.refine === Cesium3DTileRefine.REPLACE; - const skipLevelOfDetail = tileset._skipLevelOfDetail; + const skipLevelOfDetail = tileset.isSkippingLevelOfDetail; if ( (replace && !skipLevelOfDetail) || !tileset.foveatedScreenSpaceError || @@ -935,7 +985,7 @@ function getPriorityReverseScreenSpaceError(tileset, tile) { const parent = tile.parent; const useParentScreenSpaceError = defined(parent) && - (!tileset._skipLevelOfDetail || + (!tileset.isSkippingLevelOfDetail || tile._screenSpaceError === 0.0 || parent.hasTilesetContent || parent.hasImplicitContent); @@ -953,6 +1003,11 @@ function getPriorityReverseScreenSpaceError(tileset, tile) { */ Cesium3DTile.prototype.updateVisibility = function (frameState) { const { parent, tileset } = this; + if (this._updatedVisibilityFrame === tileset._updatedVisibilityFrame) { + // The tile has already been updated for this frame + return; + } + const parentTransform = defined(parent) ? parent.computedTransform : tileset.modelMatrix; @@ -983,6 +1038,8 @@ Cesium3DTile.prototype.updateVisibility = function (frameState) { this ); this.priorityDeferred = isPriorityDeferred(this, frameState); + + this._updatedVisibilityFrame = tileset._updatedVisibilityFrame; }; /** @@ -1342,11 +1399,7 @@ Cesium3DTile.prototype.cancelRequests = function () { * @private */ Cesium3DTile.prototype.unloadContent = function () { - if ( - this.hasEmptyContent || - this.hasTilesetContent || - this.hasImplicitContent - ) { + if (!this.hasRenderableContent) { return; } @@ -1720,19 +1773,15 @@ Cesium3DTile.prototype.createBoundingVolume = function ( ); } - if (defined(boundingVolumeHeader.box)) { - return createBox(boundingVolumeHeader.box, transform, result); + const { box, region, sphere } = boundingVolumeHeader; + if (defined(box)) { + return createBox(box, transform, result); } - if (defined(boundingVolumeHeader.region)) { - return createRegion( - boundingVolumeHeader.region, - transform, - this._initialTransform, - result - ); + if (defined(region)) { + return createRegion(region, transform, this._initialTransform, result); } - if (defined(boundingVolumeHeader.sphere)) { - return createSphere(boundingVolumeHeader.sphere, transform, result); + if (defined(sphere)) { + return createSphere(sphere, transform, result); } throw new RuntimeError( "boundingVolume must contain a sphere, region, or box" @@ -1824,8 +1873,6 @@ function applyDebugSettings(tile, tileset, frameState, passOptions) { const hasContentBoundingVolume = defined(tile._contentHeader) && defined(tile._contentHeader.boundingVolume); - const empty = - tile.hasEmptyContent || tile.hasTilesetContent || tile.hasImplicitContent; const showVolume = tileset.debugShowBoundingVolume || @@ -1834,7 +1881,7 @@ function applyDebugSettings(tile, tileset, frameState, passOptions) { let color; if (!tile._finalResolution) { color = Color.YELLOW; - } else if (empty) { + } else if (!tile.hasRenderableContent) { color = Color.DARKGRAY; } else { color = Color.WHITE; @@ -1981,18 +2028,18 @@ function updateClippingPlanes(tile, tileset) { * @param {object} passOptions */ Cesium3DTile.prototype.update = function (tileset, frameState, passOptions) { - const commandStart = frameState.commandList.length; + const { commandList } = frameState; + const commandStart = commandList.length; updateClippingPlanes(this, tileset); applyDebugSettings(this, tileset, frameState, passOptions); updateContent(this, tileset, frameState); - const commandEnd = frameState.commandList.length; - const commandsLength = commandEnd - commandStart; - this._commandsLength = commandsLength; + const commandEnd = commandList.length; + this._commandsLength = commandEnd - commandStart; - for (let i = 0; i < commandsLength; ++i) { - const command = frameState.commandList[commandStart + i]; + for (let i = commandStart; i < commandEnd; ++i) { + const command = commandList[i]; const translucent = command.pass === Pass.TRANSLUCENT; command.depthForTranslucentClassification = translucent; } @@ -2132,7 +2179,8 @@ Cesium3DTile.prototype.updatePriority = function () { // Map 0-1 then convert to digit. Include a distance sort when doing non-skipLOD and replacement refinement, helps things like non-skipLOD photogrammetry const useDistance = - !tileset._skipLevelOfDetail && this.refine === Cesium3DTileRefine.REPLACE; + !tileset.isSkippingLevelOfDetail && + this.refine === Cesium3DTileRefine.REPLACE; const normalizedPreferredSorting = useDistance ? priorityNormalizeAndClamp( this._priorityHolder._distanceToCamera, diff --git a/packages/engine/Source/Scene/Cesium3DTileBatchTable.js b/packages/engine/Source/Scene/Cesium3DTileBatchTable.js index a892b0f94945..6631d6c19622 100644 --- a/packages/engine/Source/Scene/Cesium3DTileBatchTable.js +++ b/packages/engine/Source/Scene/Cesium3DTileBatchTable.js @@ -896,8 +896,8 @@ Cesium3DTileBatchTable.prototype.addDerivedCommands = function ( const finalResolution = tile._finalResolution; const tileset = tile.tileset; const bivariateVisibilityTest = - tileset._skipLevelOfDetail && - tileset._hasMixedContent && + tileset.isSkippingLevelOfDetail && + tileset.hasMixedContent && frameState.context.stencilBuffer; const styleCommandsNeeded = getStyleCommandsNeeded(this); diff --git a/packages/engine/Source/Scene/Cesium3DTilePass.js b/packages/engine/Source/Scene/Cesium3DTilePass.js index c210f917734f..4d3017d30556 100644 --- a/packages/engine/Source/Scene/Cesium3DTilePass.js +++ b/packages/engine/Source/Scene/Cesium3DTilePass.js @@ -1,6 +1,3 @@ -import Cesium3DTilesetMostDetailedTraversal from "./Cesium3DTilesetMostDetailedTraversal.js"; -import Cesium3DTilesetTraversal from "./Cesium3DTilesetTraversal.js"; - /** * The pass in which a 3D Tileset is updated. * @@ -21,56 +18,56 @@ const Cesium3DTilePass = { const passOptions = new Array(Cesium3DTilePass.NUMBER_OF_PASSES); passOptions[Cesium3DTilePass.RENDER] = Object.freeze({ - traversal: Cesium3DTilesetTraversal, + pass: Cesium3DTilePass.RENDER, isRender: true, requestTiles: true, ignoreCommands: false, }); passOptions[Cesium3DTilePass.PICK] = Object.freeze({ - traversal: Cesium3DTilesetTraversal, + pass: Cesium3DTilePass.PICK, isRender: false, requestTiles: false, ignoreCommands: false, }); passOptions[Cesium3DTilePass.SHADOW] = Object.freeze({ - traversal: Cesium3DTilesetTraversal, + pass: Cesium3DTilePass.SHADOW, isRender: false, requestTiles: true, ignoreCommands: false, }); passOptions[Cesium3DTilePass.PRELOAD] = Object.freeze({ - traversal: Cesium3DTilesetTraversal, + pass: Cesium3DTilePass.SHADOW, isRender: false, requestTiles: true, ignoreCommands: true, }); passOptions[Cesium3DTilePass.PRELOAD_FLIGHT] = Object.freeze({ - traversal: Cesium3DTilesetTraversal, + pass: Cesium3DTilePass.PRELOAD_FLIGHT, isRender: false, requestTiles: true, ignoreCommands: true, }); passOptions[Cesium3DTilePass.REQUEST_RENDER_MODE_DEFER_CHECK] = Object.freeze({ - traversal: Cesium3DTilesetTraversal, + pass: Cesium3DTilePass.REQUEST_RENDER_MODE_DEFER_CHECK, isRender: false, requestTiles: true, ignoreCommands: true, }); passOptions[Cesium3DTilePass.MOST_DETAILED_PRELOAD] = Object.freeze({ - traversal: Cesium3DTilesetMostDetailedTraversal, + pass: Cesium3DTilePass.MOST_DETAILED_PRELOAD, isRender: false, requestTiles: true, ignoreCommands: true, }); passOptions[Cesium3DTilePass.MOST_DETAILED_PICK] = Object.freeze({ - traversal: Cesium3DTilesetMostDetailedTraversal, + pass: Cesium3DTilePass.MOST_DETAILED_PICK, isRender: false, requestTiles: false, ignoreCommands: false, diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 7ddd6a3da9a2..b36f2c91a06a 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -52,6 +52,9 @@ import StencilConstants from "./StencilConstants.js"; import TileBoundingRegion from "./TileBoundingRegion.js"; import TileBoundingSphere from "./TileBoundingSphere.js"; import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js"; +import Cesium3DTilesetMostDetailedTraversal from "./Cesium3DTilesetMostDetailedTraversal.js"; +import Cesium3DTilesetBaseTraversal from "./Cesium3DTilesetBaseTraversal.js"; +import Cesium3DTilesetSkipTraversal from "./Cesium3DTilesetSkipTraversal.js"; /** * @typedef {Object} Cesium3DTileset.ConstructorOptions @@ -668,7 +671,7 @@ function Cesium3DTileset(options) { * @default false */ this.skipLevelOfDetail = defaultValue(options.skipLevelOfDetail, false); - this._skipLevelOfDetail = this.skipLevelOfDetail; + this._disableSkipLevelOfDetail = false; /** @@ -1374,6 +1377,50 @@ Object.defineProperties(Cesium3DTileset.prototype, { }, }, + /** + * Whether the tileset is rendering different levels of detail in the same view. + * Only relevant if {@link Cesium3DTileset.isSkippingLevelOfDetail} is true. + * + * @memberof Cesium3DTileset.prototype + * + * @type {boolean} + * @private + */ + hasMixedContent: { + get: function () { + return this._hasMixedContent; + }, + set: function (value) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.bool("value", value); + //>>includeEnd('debug'); + + this._hasMixedContent = value; + }, + }, + + /** + * Whether this tileset is actually skipping levels of detail. + * The user option may have been disabled if all tiles are using additive refinement, + * or if some tiles have a content type for which rendering does not support skipping + * + * @memberof Cesium3DTileset.prototype + * + * @type {boolean} + * @private + * @readonly + */ + isSkippingLevelOfDetail: { + get: function () { + return ( + this.skipLevelOfDetail && + !defined(this._classificationType) && + !this._disableSkipLevelOfDetail && + !this._allTilesAdditive + ); + }, + }, + /** * The tileset's schema, groups, tileset metadata and other details from the * 3DTILES_metadata extension or a 3D Tiles 1.1 tileset JSON. This getter is @@ -2531,12 +2578,6 @@ Cesium3DTileset.prototype.prePassesUpdate = function (frameState) { 0.0 ); - this._skipLevelOfDetail = - this.skipLevelOfDetail && - !defined(this._classificationType) && - !this._disableSkipLevelOfDetail && - !this._allTilesAdditive; - if (this.dynamicScreenSpaceError) { updateDynamicScreenSpaceError(this, frameState); } @@ -2836,18 +2877,15 @@ function updateTiles(tileset, frameState, passOptions) { tileset._styleEngine.applyStyle(tileset); tileset._styleApplied = true; - const isRender = passOptions.isRender; - const { statistics, tileVisible } = tileset; - const commandList = frameState.commandList; + const { commandList, context } = frameState; const numberOfInitialCommands = commandList.length; const selectedTiles = tileset._selectedTiles; - const selectedLength = selectedTiles.length; const bivariateVisibilityTest = - tileset._skipLevelOfDetail && + tileset.isSkippingLevelOfDetail && tileset._hasMixedContent && - frameState.context.stencilBuffer && - selectedLength > 0; + context.stencilBuffer && + selectedTiles.length > 0; tileset._backfaceCommands.length = 0; @@ -2864,8 +2902,11 @@ function updateTiles(tileset, frameState, passOptions) { commandList.push(tileset._stencilClearCommand); } + const { statistics, tileVisible } = tileset; + const isRender = passOptions.isRender; const lengthBeforeUpdate = commandList.length; - for (let i = 0; i < selectedLength; ++i) { + + for (let i = 0; i < selectedTiles.length; ++i) { const tile = selectedTiles[i]; // Raise the tileVisible event before update in case the tileVisible event // handler makes changes that update needs to apply to WebGL resources @@ -3142,8 +3183,6 @@ function update(tileset, frameState, passStatistics, passOptions) { const statistics = tileset._statistics; statistics.clear(); - const isRender = passOptions.isRender; - // Resets the visibility check for each pass ++tileset._updatedVisibilityFrame; @@ -3154,7 +3193,9 @@ function update(tileset, frameState, passStatistics, passOptions) { tileset._cullRequestsWhileMoving = tileset.cullRequestsWhileMoving && !tileset._modelMatrixChanged; - const ready = passOptions.traversal.selectTiles(tileset, frameState); + const ready = tileset + .getTraversal(passOptions) + .selectTiles(tileset, frameState); if (passOptions.requestTiles) { requestTiles(tileset); @@ -3165,7 +3206,7 @@ function update(tileset, frameState, passStatistics, passOptions) { // Update pass statistics Cesium3DTilesetStatistics.clone(statistics, passStatistics); - if (isRender) { + if (passOptions.isRender) { const credits = tileset._credits; if (defined(credits) && statistics.selected !== 0) { for (let i = 0; i < credits.length; ++i) { @@ -3179,6 +3220,24 @@ function update(tileset, frameState, passStatistics, passOptions) { return ready; } +/** + * @private + * @param {object} passOptions + * @returns {Cesium3DTilesetTraversal} + */ +Cesium3DTileset.prototype.getTraversal = function (passOptions) { + const { pass } = passOptions; + if ( + pass === Cesium3DTilePass.MOST_DETAILED_PRELOAD || + pass === Cesium3DTilePass.MOST_DETAILED_PICK + ) { + return Cesium3DTilesetMostDetailedTraversal; + } + return this.isSkippingLevelOfDetail + ? Cesium3DTilesetSkipTraversal + : Cesium3DTilesetBaseTraversal; +}; + /** * @private * @param {FrameState} frameState diff --git a/packages/engine/Source/Scene/Cesium3DTilesetBaseTraversal.js b/packages/engine/Source/Scene/Cesium3DTilesetBaseTraversal.js new file mode 100644 index 000000000000..a85ddcc4013c --- /dev/null +++ b/packages/engine/Source/Scene/Cesium3DTilesetBaseTraversal.js @@ -0,0 +1,301 @@ +import defined from "../Core/defined.js"; +import ManagedArray from "../Core/ManagedArray.js"; +import Cesium3DTileRefine from "./Cesium3DTileRefine.js"; +import Cesium3DTilesetTraversal from "./Cesium3DTilesetTraversal.js"; + +/** + * Depth-first traversal that traverses all visible tiles and marks tiles for selection. + * A tile does not refine until all children are loaded. + * This is the traditional replacement refinement approach and is called the base traversal. + * + * @alias Cesium3DTilesetBaseTraversal + * @constructor + * + * @private + */ +function Cesium3DTilesetBaseTraversal() {} + +const traversal = { + stack: new ManagedArray(), + stackMaximumLength: 0, +}; + +const emptyTraversal = { + stack: new ManagedArray(), + stackMaximumLength: 0, +}; + +/** + * Traverses a {@link Cesium3DTileset} to determine which tiles to load and render. + * + * @private + * @param {Cesium3DTileset} tileset + * @param {FrameState} frameState + */ +Cesium3DTilesetBaseTraversal.selectTiles = function (tileset, frameState) { + tileset._requestedTiles.length = 0; + + if (tileset.debugFreezeFrame) { + return; + } + + tileset._selectedTiles.length = 0; + tileset._selectedTilesToStyle.length = 0; + tileset._emptyTiles.length = 0; + tileset.hasMixedContent = false; + + const root = tileset.root; + Cesium3DTilesetTraversal.updateTile(root, frameState); + + if (!root.isVisible) { + return; + } + + if ( + root.getScreenSpaceError(frameState, true) <= + tileset._maximumScreenSpaceError + ) { + return; + } + + executeTraversal(root, frameState); + + traversal.stack.trim(traversal.stackMaximumLength); + emptyTraversal.stack.trim(emptyTraversal.stackMaximumLength); + + // Update the priority for any requests found during traversal + // Update after traversal so that min and max values can be used to normalize priority values + const requestedTiles = tileset._requestedTiles; + for (let i = 0; i < requestedTiles.length; ++i) { + requestedTiles[i].updatePriority(); + } +}; + +/** + * Mark a tile as selected if it has content available. + * + * @private + * @param {Cesium3DTile} tile + * @param {FrameState} frameState + */ +function selectDesiredTile(tile, frameState) { + if (tile.contentAvailable) { + Cesium3DTilesetTraversal.selectTile(tile, frameState); + } +} + +/** + * @private + * @param {Cesium3DTile} tile + * @param {ManagedArray} stack + * @param {FrameState} frameState + * @returns {boolean} + */ +function updateAndPushChildren(tile, stack, frameState) { + const replace = tile.refine === Cesium3DTileRefine.REPLACE; + const { tileset, children } = tile; + const { updateTile, loadTile, touchTile } = Cesium3DTilesetTraversal; + + for (let i = 0; i < children.length; ++i) { + updateTile(children[i], frameState); + } + + // Sort by distance to take advantage of early Z and reduce artifacts for skipLevelOfDetail + children.sort(Cesium3DTilesetTraversal.sortChildrenByDistanceToCamera); + + // For traditional replacement refinement only refine if all children are loaded. + // Empty tiles are exempt since it looks better if children stream in as they are loaded to fill the empty space. + const checkRefines = replace && tile.hasRenderableContent; + let refines = true; + + let anyChildrenVisible = false; + + // Determining min child + let minIndex = -1; + let minimumPriority = Number.MAX_VALUE; + + for (let i = 0; i < children.length; ++i) { + const child = children[i]; + if (child.isVisible) { + stack.push(child); + if (child._foveatedFactor < minimumPriority) { + minIndex = i; + minimumPriority = child._foveatedFactor; + } + anyChildrenVisible = true; + } else if (checkRefines || tileset.loadSiblings) { + // Keep non-visible children loaded since they are still needed before the parent can refine. + // Or loadSiblings is true so always load tiles regardless of visibility. + if (child._foveatedFactor < minimumPriority) { + minIndex = i; + minimumPriority = child._foveatedFactor; + } + loadTile(child, frameState); + touchTile(child, frameState); + } + if (checkRefines) { + let childRefines; + if (!child._inRequestVolume) { + childRefines = false; + } else if (!child.hasRenderableContent) { + childRefines = executeEmptyTraversal(child, frameState); + } else { + childRefines = child.contentAvailable; + } + refines = refines && childRefines; + } + } + + if (!anyChildrenVisible) { + refines = false; + } + + if (minIndex !== -1 && replace) { + // An ancestor will hold the _foveatedFactor and _distanceToCamera for descendants between itself and its highest priority descendant. Siblings of a min children along the way use this ancestor as their priority holder as well. + // Priority of all tiles that refer to the _foveatedFactor and _distanceToCamera stored in the common ancestor will be differentiated based on their _depth. + const minPriorityChild = children[minIndex]; + minPriorityChild._wasMinPriorityChild = true; + const priorityHolder = + (tile._wasMinPriorityChild || tile === tileset.root) && + minimumPriority <= tile._priorityHolder._foveatedFactor + ? tile._priorityHolder + : tile; // This is where priority dependency chains are wired up or started anew. + priorityHolder._foveatedFactor = Math.min( + minPriorityChild._foveatedFactor, + priorityHolder._foveatedFactor + ); + priorityHolder._distanceToCamera = Math.min( + minPriorityChild._distanceToCamera, + priorityHolder._distanceToCamera + ); + + for (let i = 0; i < children.length; ++i) { + children[i]._priorityHolder = priorityHolder; + } + } + + return refines; +} + +/** + * Depth-first traversal that traverses all visible tiles and marks tiles for selection. + * A tile does not refine until all children are loaded. + * This is the traditional replacement refinement approach and is called the base traversal. + * + * @private + * @param {Cesium3DTile} root + * @param {FrameState} frameState + */ +function executeTraversal(root, frameState) { + const { tileset } = root; + + const { + canTraverse, + loadTile, + visitTile, + touchTile, + } = Cesium3DTilesetTraversal; + const stack = traversal.stack; + stack.push(root); + + while (stack.length > 0) { + traversal.stackMaximumLength = Math.max( + traversal.stackMaximumLength, + stack.length + ); + + const tile = stack.pop(); + + const parent = tile.parent; + const parentRefines = !defined(parent) || parent._refines; + + tile._refines = canTraverse(tile) + ? updateAndPushChildren(tile, stack, frameState) && parentRefines + : false; + + const stoppedRefining = !tile._refines && parentRefines; + + if (!tile.hasRenderableContent) { + // Add empty tile just to show its debug bounding volume + // If the tile has tileset content load the external tileset + tileset._emptyTiles.push(tile); + loadTile(tile, frameState); + if (stoppedRefining) { + selectDesiredTile(tile, frameState); + } + } else if (tile.refine === Cesium3DTileRefine.ADD) { + // Additive tiles are always loaded and selected + selectDesiredTile(tile, frameState); + loadTile(tile, frameState); + } else if (tile.refine === Cesium3DTileRefine.REPLACE) { + loadTile(tile, frameState); + if (stoppedRefining) { + selectDesiredTile(tile, frameState); + } + } + + visitTile(tile, frameState); + touchTile(tile, frameState); + } +} + +/** + * Depth-first traversal that checks if all nearest descendants with content are loaded. + * Ignores visibility. + * + * @private + * @param {Cesium3DTile} root + * @param {FrameState} frameState + * @returns {boolean} + */ +function executeEmptyTraversal(root, frameState) { + const { + canTraverse, + updateTile, + loadTile, + touchTile, + } = Cesium3DTilesetTraversal; + let allDescendantsLoaded = true; + const stack = emptyTraversal.stack; + stack.push(root); + + while (stack.length > 0) { + emptyTraversal.stackMaximumLength = Math.max( + emptyTraversal.stackMaximumLength, + stack.length + ); + + const tile = stack.pop(); + const children = tile.children; + const childrenLength = children.length; + + // Only traverse if the tile is empty - traversal stops at descendants with content + const traverse = !tile.hasRenderableContent && canTraverse(tile); + const emptyLeaf = !tile.hasRenderableContent && tile.children.length === 0; + + // Traversal stops but the tile does not have content yet + // There will be holes if the parent tries to refine to its children, so don't refine + // One exception: a parent may refine even if one of its descendants is an empty leaf + if (!traverse && !tile.contentAvailable && !emptyLeaf) { + allDescendantsLoaded = false; + } + + updateTile(tile, frameState); + if (!tile.isVisible) { + // Load tiles that aren't visible since they are still needed for the parent to refine + loadTile(tile, frameState); + touchTile(tile, frameState); + } + + if (traverse) { + for (let i = 0; i < childrenLength; ++i) { + const child = children[i]; + stack.push(child); + } + } + } + + return allDescendantsLoaded; +} + +export default Cesium3DTilesetBaseTraversal; diff --git a/packages/engine/Source/Scene/Cesium3DTilesetMostDetailedTraversal.js b/packages/engine/Source/Scene/Cesium3DTilesetMostDetailedTraversal.js index 7fe1e10ba204..8645642a852f 100644 --- a/packages/engine/Source/Scene/Cesium3DTilesetMostDetailedTraversal.js +++ b/packages/engine/Source/Scene/Cesium3DTilesetMostDetailedTraversal.js @@ -1,11 +1,15 @@ import Intersect from "../Core/Intersect.js"; import ManagedArray from "../Core/ManagedArray.js"; import Cesium3DTileRefine from "./Cesium3DTileRefine.js"; +import Cesium3DTilesetTraversal from "./Cesium3DTilesetTraversal.js"; /** * Traversal that loads all leaves that intersect the camera frustum. * Used to determine ray-tileset intersections during a pickFromRayMostDetailed call. * + * @alias Cesium3DTilesetMostDetailedTraversal + * @constructor + * * @private */ function Cesium3DTilesetMostDetailedTraversal() {} @@ -15,25 +19,35 @@ const traversal = { stackMaximumLength: 0, }; +/** + * Traverses a {@link Cesium3DTileset} to determine which tiles to load and render. + * + * @private + * @param {Cesium3DTileset} tileset + * @param {FrameState} frameState + * @returns {boolean} Whether the appropriate tile is ready for picking + */ Cesium3DTilesetMostDetailedTraversal.selectTiles = function ( tileset, frameState ) { tileset._selectedTiles.length = 0; tileset._requestedTiles.length = 0; - tileset._hasMixedContent = false; + tileset.hasMixedContent = false; let ready = true; const root = tileset.root; root.updateVisibility(frameState); - if (!isVisible(root)) { + if (!root.isVisible) { return ready; } + const { touchTile, visitTile } = Cesium3DTilesetTraversal; + const stack = traversal.stack; - stack.push(tileset.root); + stack.push(root); while (stack.length > 0) { traversal.stackMaximumLength = Math.max( @@ -44,23 +58,23 @@ Cesium3DTilesetMostDetailedTraversal.selectTiles = function ( const tile = stack.pop(); const add = tile.refine === Cesium3DTileRefine.ADD; const replace = tile.refine === Cesium3DTileRefine.REPLACE; - const traverse = canTraverse(tileset, tile); + const traverse = canTraverse(tile); if (traverse) { - updateAndPushChildren(tileset, tile, stack, frameState); + updateAndPushChildren(tile, stack, frameState); } if (add || (replace && !traverse)) { loadTile(tileset, tile); - touchTile(tileset, tile, frameState); - selectDesiredTile(tileset, tile, frameState); + touchTile(tile, frameState); + selectDesiredTile(tile, frameState); - if (!hasEmptyContent(tile) && !tile.contentAvailable) { + if (tile.hasRenderableContent && !tile.contentAvailable) { ready = false; } } - visitTile(tileset); + visitTile(tile, frameState); } traversal.stack.trim(traversal.stackMaximumLength); @@ -68,21 +82,7 @@ Cesium3DTilesetMostDetailedTraversal.selectTiles = function ( return ready; }; -function isVisible(tile) { - return tile._visible && tile._inRequestVolume; -} - -function hasEmptyContent(tile) { - return ( - tile.hasEmptyContent || tile.hasTilesetContent || tile.hasImplicitContent - ); -} - -function hasUnloadedContent(tile) { - return !hasEmptyContent(tile) && tile.contentUnloaded; -} - -function canTraverse(tileset, tile) { +function canTraverse(tile) { if (tile.children.length === 0) { return false; } @@ -97,48 +97,35 @@ function canTraverse(tileset, tile) { return true; } - return true; // Keep traversing until a leave is hit + return true; // Keep traversing until a leaf is hit } -function updateAndPushChildren(tileset, tile, stack, frameState) { - const children = tile.children; - const length = children.length; +function updateAndPushChildren(tile, stack, frameState) { + const { children } = tile; - for (let i = 0; i < length; ++i) { + for (let i = 0; i < children.length; ++i) { const child = children[i]; child.updateVisibility(frameState); - if (isVisible(child)) { + if (child.isVisible) { stack.push(child); } } } function loadTile(tileset, tile) { - if (hasUnloadedContent(tile) || tile.contentExpired) { + if (tile.hasUnloadedRenderableContent || tile.contentExpired) { tile._priority = 0.0; // Highest priority tileset._requestedTiles.push(tile); } } -function touchTile(tileset, tile, frameState) { - if (tile._touchedFrame === frameState.frameNumber) { - // Prevents another pass from touching the frame again - return; - } - tileset._cache.touch(tile); - tile._touchedFrame = frameState.frameNumber; -} - -function visitTile(tileset) { - ++tileset.statistics.visited; -} - -function selectDesiredTile(tileset, tile, frameState) { +function selectDesiredTile(tile, frameState) { if ( tile.contentAvailable && tile.contentVisibility(frameState) !== Intersect.OUTSIDE ) { - tileset._selectedTiles.push(tile); + tile.tileset._selectedTiles.push(tile); } } + export default Cesium3DTilesetMostDetailedTraversal; diff --git a/packages/engine/Source/Scene/Cesium3DTilesetSkipTraversal.js b/packages/engine/Source/Scene/Cesium3DTilesetSkipTraversal.js new file mode 100644 index 000000000000..aea34eee3fcb --- /dev/null +++ b/packages/engine/Source/Scene/Cesium3DTilesetSkipTraversal.js @@ -0,0 +1,421 @@ +import defined from "../Core/defined.js"; +import ManagedArray from "../Core/ManagedArray.js"; +import Cesium3DTileRefine from "./Cesium3DTileRefine.js"; +import Cesium3DTilesetTraversal from "./Cesium3DTilesetTraversal.js"; + +/** + * Depth-first traversal that traverses all visible tiles and marks tiles for selection. + * Allows for skipping levels of the tree and rendering children and parent tiles simultaneously. + * + * @alias Cesium3DTilesetSkipTraversal + * @constructor + * + * @private + */ +function Cesium3DTilesetSkipTraversal() {} + +const traversal = { + stack: new ManagedArray(), + stackMaximumLength: 0, +}; + +const descendantTraversal = { + stack: new ManagedArray(), + stackMaximumLength: 0, +}; + +const selectionTraversal = { + stack: new ManagedArray(), + stackMaximumLength: 0, + ancestorStack: new ManagedArray(), + ancestorStackMaximumLength: 0, +}; + +const descendantSelectionDepth = 2; + +/** + * Traverses a {@link Cesium3DTileset} to determine which tiles to load and render. + * + * @private + * @param {Cesium3DTileset} tileset + * @param {FrameState} frameState + */ +Cesium3DTilesetSkipTraversal.selectTiles = function (tileset, frameState) { + tileset._requestedTiles.length = 0; + + if (tileset.debugFreezeFrame) { + return; + } + + tileset._selectedTiles.length = 0; + tileset._selectedTilesToStyle.length = 0; + tileset._emptyTiles.length = 0; + tileset.hasMixedContent = false; + + const root = tileset.root; + Cesium3DTilesetTraversal.updateTile(root, frameState); + + if (!root.isVisible) { + return; + } + + if ( + root.getScreenSpaceError(frameState, true) <= + tileset._maximumScreenSpaceError + ) { + return; + } + + executeTraversal(root, frameState); + traverseAndSelect(root, frameState); + + traversal.stack.trim(traversal.stackMaximumLength); + descendantTraversal.stack.trim(descendantTraversal.stackMaximumLength); + selectionTraversal.stack.trim(selectionTraversal.stackMaximumLength); + selectionTraversal.ancestorStack.trim( + selectionTraversal.ancestorStackMaximumLength + ); + + // Update the priority for any requests found during traversal + // Update after traversal so that min and max values can be used to normalize priority values + const requestedTiles = tileset._requestedTiles; + for (let i = 0; i < requestedTiles.length; ++i) { + requestedTiles[i].updatePriority(); + } +}; + +/** + * Mark descendant tiles for rendering, and update as needed + * + * @private + * @param {Cesium3DTile} root + * @param {FrameState} frameState + */ +function selectDescendants(root, frameState) { + const { updateTile, touchTile, selectTile } = Cesium3DTilesetTraversal; + const stack = descendantTraversal.stack; + stack.push(root); + while (stack.length > 0) { + descendantTraversal.stackMaximumLength = Math.max( + descendantTraversal.stackMaximumLength, + stack.length + ); + const tile = stack.pop(); + const children = tile.children; + for (let i = 0; i < children.length; ++i) { + const child = children[i]; + if (child.isVisible) { + if (child.contentAvailable) { + updateTile(child, frameState); + touchTile(child, frameState); + selectTile(child, frameState); + } else if (child._depth - root._depth < descendantSelectionDepth) { + // Continue traversing, but not too far + stack.push(child); + } + } + } + } +} + +/** + * Mark a tile as selected if it has content available. + * If its content is not available, and we are skipping levels of detail, + * select an ancestor or descendant tile instead + * + * @private + * @param {Cesium3DTile} tile + * @param {FrameState} frameState + */ +function selectDesiredTile(tile, frameState) { + // If this tile is not loaded attempt to select its ancestor instead + const loadedTile = tile.contentAvailable + ? tile + : tile._ancestorWithContentAvailable; + if (defined(loadedTile)) { + // Tiles will actually be selected in traverseAndSelect + loadedTile._shouldSelect = true; + } else { + // If no ancestors are ready traverse down and select tiles to minimize empty regions. + // This happens often for immediatelyLoadDesiredLevelOfDetail where parent tiles are not necessarily loaded before zooming out. + selectDescendants(tile, frameState); + } +} + +/** + * Update links to the ancestor tiles that have content + * + * @private + * @param {Cesium3DTile} tile + * @param {FrameState} frameState + */ +function updateTileAncestorContentLinks(tile, frameState) { + tile._ancestorWithContent = undefined; + tile._ancestorWithContentAvailable = undefined; + + const { parent } = tile; + if (!defined(parent)) { + return; + } + const parentHasContent = + !parent.hasUnloadedRenderableContent || + parent._requestedFrame === frameState.frameNumber; + + // ancestorWithContent is an ancestor that has content or has the potential to have + // content. Used in conjunction with tileset.skipLevels to know when to skip a tile. + tile._ancestorWithContent = parentHasContent + ? parent + : parent._ancestorWithContent; + + // ancestorWithContentAvailable is an ancestor that is rendered if a desired tile is not loaded + tile._ancestorWithContentAvailable = parent.contentAvailable + ? parent + : parent._ancestorWithContentAvailable; +} + +/** + * Determine if a tile has reached the limit of level of detail skipping. + * If so, it should _not_ be skipped: it should be loaded and rendered + * + * @private + * @param {Cesium3DTileset} tileset + * @param {Cesium3DTile} tile + * @returns {boolean} true if this tile should not be skipped + */ +function reachedSkippingThreshold(tileset, tile) { + const ancestor = tile._ancestorWithContent; + return ( + !tileset.immediatelyLoadDesiredLevelOfDetail && + (tile._priorityProgressiveResolutionScreenSpaceErrorLeaf || + (defined(ancestor) && + tile._screenSpaceError < + ancestor._screenSpaceError / tileset.skipScreenSpaceErrorFactor && + tile._depth > ancestor._depth + tileset.skipLevels)) + ); +} + +/** + * @private + * @param {Cesium3DTile} tile + * @param {ManagedArray} stack + * @param {FrameState} frameState + * @returns {boolean} + */ +function updateAndPushChildren(tile, stack, frameState) { + const { tileset, children } = tile; + const { updateTile, loadTile, touchTile } = Cesium3DTilesetTraversal; + + for (let i = 0; i < children.length; ++i) { + updateTile(children[i], frameState); + } + + // Sort by distance to take advantage of early Z and reduce artifacts + children.sort(Cesium3DTilesetTraversal.sortChildrenByDistanceToCamera); + + let anyChildrenVisible = false; + + for (let i = 0; i < children.length; ++i) { + const child = children[i]; + if (child.isVisible) { + stack.push(child); + anyChildrenVisible = true; + } else if (tileset.loadSiblings) { + loadTile(child, frameState); + touchTile(child, frameState); + } + } + + return anyChildrenVisible; +} + +/** + * Determine if a tile is part of the base traversal. + * If not, this tile could be considered for level of detail skipping + * + * @private + * @param {Cesium3DTile} tile + * @param {number} baseScreenSpaceError + * @returns {boolean} + */ +function inBaseTraversal(tile, baseScreenSpaceError) { + const { tileset } = tile; + if (tileset.immediatelyLoadDesiredLevelOfDetail) { + return false; + } + if (!defined(tile._ancestorWithContent)) { + // Include root or near-root tiles in the base traversal so there is something to select up to + return true; + } + if (tile._screenSpaceError === 0.0) { + // If a leaf, use parent's SSE + return tile.parent._screenSpaceError > baseScreenSpaceError; + } + return tile._screenSpaceError > baseScreenSpaceError; +} + +/** + * Depth-first traversal that traverses all visible tiles and marks tiles for selection. + * Tiles that have a greater screen space error than the base screen space error are part of the base traversal, + * all other tiles are part of the skip traversal. The skip traversal allows for skipping levels of the tree + * and rendering children and parent tiles simultaneously. + * + * @private + * @param {Cesium3DTile} root + * @param {FrameState} frameState + */ +function executeTraversal(root, frameState) { + const { tileset } = root; + const baseScreenSpaceError = tileset.immediatelyLoadDesiredLevelOfDetail + ? Number.MAX_VALUE + : Math.max(tileset.baseScreenSpaceError, tileset.maximumScreenSpaceError); + const { + canTraverse, + loadTile, + visitTile, + touchTile, + } = Cesium3DTilesetTraversal; + const stack = traversal.stack; + stack.push(root); + + while (stack.length > 0) { + traversal.stackMaximumLength = Math.max( + traversal.stackMaximumLength, + stack.length + ); + + const tile = stack.pop(); + + updateTileAncestorContentLinks(tile, frameState); + const parent = tile.parent; + const parentRefines = !defined(parent) || parent._refines; + + tile._refines = canTraverse(tile) + ? updateAndPushChildren(tile, stack, frameState) && parentRefines + : false; + + const stoppedRefining = !tile._refines && parentRefines; + + if (!tile.hasRenderableContent) { + // Add empty tile just to show its debug bounding volume + // If the tile has tileset content load the external tileset + // If the tile cannot refine further select its nearest loaded ancestor + tileset._emptyTiles.push(tile); + loadTile(tile, frameState); + if (stoppedRefining) { + selectDesiredTile(tile, frameState); + } + } else if (tile.refine === Cesium3DTileRefine.ADD) { + // Additive tiles are always loaded and selected + selectDesiredTile(tile, frameState); + loadTile(tile, frameState); + } else if (tile.refine === Cesium3DTileRefine.REPLACE) { + if (inBaseTraversal(tile, baseScreenSpaceError)) { + // Always load tiles in the base traversal + // Select tiles that can't refine further + loadTile(tile, frameState); + if (stoppedRefining) { + selectDesiredTile(tile, frameState); + } + } else if (stoppedRefining) { + // In skip traversal, load and select tiles that can't refine further + selectDesiredTile(tile, frameState); + loadTile(tile, frameState); + } else if (reachedSkippingThreshold(tileset, tile)) { + // In skip traversal, load tiles that aren't skipped + loadTile(tile, frameState); + } + } + + visitTile(tile, frameState); + touchTile(tile, frameState); + } +} + +/** + * Traverse the tree and check if their selected frame is the current frame. If so, add it to a selection queue. + * This is a preorder traversal so children tiles are selected before ancestor tiles. + * + * The reason for the preorder traversal is so that tiles can easily be marked with their + * selection depth. A tile's _selectionDepth is its depth in the tree where all non-selected tiles are removed. + * This property is important for use in the stencil test because we want to render deeper tiles on top of their + * ancestors. If a tileset is very deep, the depth is unlikely to fit into the stencil buffer. + * + * We want to select children before their ancestors because there is no guarantee on the relationship between + * the children's z-depth and the ancestor's z-depth. We cannot rely on Z because we want the child to appear on top + * of ancestor regardless of true depth. The stencil tests used require children to be drawn first. + * + * NOTE: 3D Tiles uses 3 bits from the stencil buffer meaning this will not work when there is a chain of + * selected tiles that is deeper than 7. This is not very likely. + * + * @private + * @param {Cesium3DTile} root + * @param {FrameState} frameState + */ +function traverseAndSelect(root, frameState) { + const { selectTile, canTraverse } = Cesium3DTilesetTraversal; + const { stack, ancestorStack } = selectionTraversal; + let lastAncestor; + + stack.push(root); + + while (stack.length > 0 || ancestorStack.length > 0) { + selectionTraversal.stackMaximumLength = Math.max( + selectionTraversal.stackMaximumLength, + stack.length + ); + selectionTraversal.ancestorStackMaximumLength = Math.max( + selectionTraversal.ancestorStackMaximumLength, + ancestorStack.length + ); + + if (ancestorStack.length > 0) { + const waitingTile = ancestorStack.peek(); + if (waitingTile._stackLength === stack.length) { + ancestorStack.pop(); + if (waitingTile !== lastAncestor) { + waitingTile._finalResolution = false; + } + selectTile(waitingTile, frameState); + continue; + } + } + + const tile = stack.pop(); + if (!defined(tile)) { + // stack is empty but ancestorStack isn't + continue; + } + + const traverse = canTraverse(tile); + + if (tile._shouldSelect) { + if (tile.refine === Cesium3DTileRefine.ADD) { + selectTile(tile, frameState); + } else { + tile._selectionDepth = ancestorStack.length; + if (tile._selectionDepth > 0) { + tile.tileset.hasMixedContent = true; + } + lastAncestor = tile; + if (!traverse) { + selectTile(tile, frameState); + continue; + } + ancestorStack.push(tile); + tile._stackLength = stack.length; + } + } + + if (traverse) { + const children = tile.children; + for (let i = 0; i < children.length; ++i) { + const child = children[i]; + if (child.isVisible) { + stack.push(child); + } + } + } + } +} + +export default Cesium3DTilesetSkipTraversal; diff --git a/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js b/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js index 7da76155fed3..fc30a1eca829 100644 --- a/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js +++ b/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js @@ -1,118 +1,71 @@ import defined from "../Core/defined.js"; +import DeveloperError from "../Core/DeveloperError.js"; import Intersect from "../Core/Intersect.js"; -import ManagedArray from "../Core/ManagedArray.js"; import Cesium3DTileOptimizationHint from "./Cesium3DTileOptimizationHint.js"; import Cesium3DTileRefine from "./Cesium3DTileRefine.js"; /** + * Traverses a {@link Cesium3DTileset} to determine which tiles to load and render. + * This type describes an interface and is not intended to be instantiated directly. + * + * @alias Cesium3DTilesetTraversal + * @constructor + * @abstract + * + * @see Cesium3DTilesetBaseTraversal + * @see Cesium3DTilesetSkipTraversal + * @see Cesium3DTilesetMostDetailedTraversal + * * @private */ function Cesium3DTilesetTraversal() {} -const traversal = { - stack: new ManagedArray(), - stackMaximumLength: 0, -}; - -const emptyTraversal = { - stack: new ManagedArray(), - stackMaximumLength: 0, -}; - -const descendantTraversal = { - stack: new ManagedArray(), - stackMaximumLength: 0, -}; - -const selectionTraversal = { - stack: new ManagedArray(), - stackMaximumLength: 0, - ancestorStack: new ManagedArray(), - ancestorStackMaximumLength: 0, -}; - -const descendantSelectionDepth = 2; - /** + * Traverses a {@link Cesium3DTileset} to determine which tiles to load and render. + * * @private * @param {Cesium3DTileset} tileset * @param {FrameState} frameState */ Cesium3DTilesetTraversal.selectTiles = function (tileset, frameState) { - tileset._requestedTiles.length = 0; - - if (tileset.debugFreezeFrame) { - return; - } - - tileset._selectedTiles.length = 0; - tileset._selectedTilesToStyle.length = 0; - tileset._emptyTiles.length = 0; - tileset._hasMixedContent = false; - - const root = tileset.root; - updateTile(root, frameState); - - if (!isVisible(root)) { - return; - } - - if ( - root.getScreenSpaceError(frameState, true) <= - tileset._maximumScreenSpaceError - ) { - return; - } - - const baseScreenSpaceError = !skipLevelOfDetail(tileset) - ? tileset._maximumScreenSpaceError - : tileset.immediatelyLoadDesiredLevelOfDetail - ? Number.MAX_VALUE - : Math.max(tileset.baseScreenSpaceError, tileset.maximumScreenSpaceError); - - executeTraversal(root, baseScreenSpaceError, frameState); - - if (skipLevelOfDetail(tileset)) { - traverseAndSelect(root, frameState); - } - - traversal.stack.trim(traversal.stackMaximumLength); - emptyTraversal.stack.trim(emptyTraversal.stackMaximumLength); - descendantTraversal.stack.trim(descendantTraversal.stackMaximumLength); - selectionTraversal.stack.trim(selectionTraversal.stackMaximumLength); - selectionTraversal.ancestorStack.trim( - selectionTraversal.ancestorStackMaximumLength - ); - - // Update the priority for any requests found during traversal - // Update after traversal so that min and max values can be used to normalize priority values - const requestedTiles = tileset._requestedTiles; - for (let i = 0; i < requestedTiles.length; ++i) { - requestedTiles[i].updatePriority(); - } + DeveloperError.throwInstantiationError(); }; /** + * Sort by farthest child first since this is going on a stack + * * @private - * @param {Cesium3DTile} tile - * @returns {boolean} Whether the tile is within the current field of view + * @param {Cesium3DTile} a + * @param {Cesium3DTile} b + * @returns {number} */ -function isVisible(tile) { - return tile._visible && tile._inRequestVolume; -} +Cesium3DTilesetTraversal.sortChildrenByDistanceToCamera = function (a, b) { + if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { + return b._centerZDepth - a._centerZDepth; + } + + return b._distanceToCamera - a._distanceToCamera; +}; /** - * The private ._skipLevelOfDetail flag on a Cesium3DTileset is updated in - * Cesium3DTileset.prototype.prePassesUpdate to confirm if skipping is actually - * possible and allowed, even when the public .skipLevelOfDetail flag is true + * Determine if a tile can and should be traversed for children tiles that + * would contribute to rendering the current view * * @private - * @param {Cesium3DTileset} tileset - * @returns {boolean} Whether to do LOD skipping + * @param {Cesium3DTile} tile + * @returns {boolean} */ -function skipLevelOfDetail(tileset) { - return tileset._skipLevelOfDetail; -} +Cesium3DTilesetTraversal.canTraverse = function (tile) { + if (tile.children.length === 0) { + return false; + } + if (tile.hasTilesetContent || tile.hasImplicitContent) { + // Traverse external tileset to visit its root tile + // Don't traverse if the subtree is expired because it will be destroyed + return !tile.contentExpired; + } + return tile._screenSpaceError > tile.tileset._maximumScreenSpaceError; +}; /** * Mark a tile as selected, and add it to the tileset's list of selected tiles @@ -121,7 +74,7 @@ function skipLevelOfDetail(tileset) { * @param {Cesium3DTile} tile * @param {FrameState} frameState */ -function selectTile(tile, frameState) { +Cesium3DTilesetTraversal.selectTile = function (tile, frameState) { if (tile.contentVisibility(frameState) === Intersect.OUTSIDE) { return; } @@ -138,135 +91,61 @@ function selectTile(tile, frameState) { } tile._selectedFrame = frameState.frameNumber; tileset._selectedTiles.push(tile); -} - -/** - * Mark descendant tiles for rendering, and update as needed - * - * @private - * @param {Cesium3DTile} root - * @param {FrameState} frameState - */ -function selectDescendants(root, frameState) { - const stack = descendantTraversal.stack; - stack.push(root); - while (stack.length > 0) { - descendantTraversal.stackMaximumLength = Math.max( - descendantTraversal.stackMaximumLength, - stack.length - ); - const tile = stack.pop(); - const children = tile.children; - for (let i = 0; i < children.length; ++i) { - const child = children[i]; - if (isVisible(child)) { - if (child.contentAvailable) { - updateTile(child, frameState); - touchTile(child, frameState); - selectTile(child, frameState); - } else if (child._depth - root._depth < descendantSelectionDepth) { - // Continue traversing, but not too far - stack.push(child); - } - } - } - } -} - -/** - * Mark a tile as selected if it has content available. - * If its content is not available, and we are skipping levels of detail, - * select an ancestor or descendant tile instead - * - * @private - * @param {Cesium3DTile} tile - * @param {FrameState} frameState - */ -function selectDesiredTile(tile, frameState) { - if (!skipLevelOfDetail(tile.tileset)) { - if (tile.contentAvailable) { - // The tile can be selected right away and does not require traverseAndSelect - selectTile(tile, frameState); - } - return; - } - - // If this tile is not loaded attempt to select its ancestor instead - const loadedTile = tile.contentAvailable - ? tile - : tile._ancestorWithContentAvailable; - if (defined(loadedTile)) { - // Tiles will actually be selected in traverseAndSelect - loadedTile._shouldSelect = true; - } else { - // If no ancestors are ready traverse down and select tiles to minimize empty regions. - // This happens often for immediatelyLoadDesiredLevelOfDetail where parent tiles are not necessarily loaded before zooming out. - selectDescendants(tile, frameState); - } -} +}; /** * @private * @param {Cesium3DTile} tile * @param {FrameState} frameState */ -function visitTile(tile, frameState) { +Cesium3DTilesetTraversal.visitTile = function (tile, frameState) { ++tile.tileset._statistics.visited; tile._visitedFrame = frameState.frameNumber; -} +}; /** * @private * @param {Cesium3DTile} tile * @param {FrameState} frameState */ -function touchTile(tile, frameState) { +Cesium3DTilesetTraversal.touchTile = function (tile, frameState) { if (tile._touchedFrame === frameState.frameNumber) { // Prevents another pass from touching the frame again return; } tile.tileset._cache.touch(tile); tile._touchedFrame = frameState.frameNumber; -} +}; /** + * Add a tile to the list of requested tiles, if appropriate + * * @private * @param {Cesium3DTile} tile + * @param {FrameState} frameState */ -function updateMinimumMaximumPriority(tile) { - const { - _maximumPriority: maximumPriority, - _minimumPriority: minimumPriority, - } = tile.tileset; - const priorityHolder = tile._priorityHolder; +Cesium3DTilesetTraversal.loadTile = function (tile, frameState) { + const { tileset } = tile; + if ( + tile._requestedFrame === frameState.frameNumber || + (!tile.hasUnloadedRenderableContent && !tile.contentExpired) + ) { + return; + } - maximumPriority.distance = Math.max( - priorityHolder._distanceToCamera, - maximumPriority.distance - ); - minimumPriority.distance = Math.min( - priorityHolder._distanceToCamera, - minimumPriority.distance - ); - maximumPriority.depth = Math.max(tile._depth, maximumPriority.depth); - minimumPriority.depth = Math.min(tile._depth, minimumPriority.depth); - maximumPriority.foveatedFactor = Math.max( - priorityHolder._foveatedFactor, - maximumPriority.foveatedFactor - ); - minimumPriority.foveatedFactor = Math.min( - priorityHolder._foveatedFactor, - minimumPriority.foveatedFactor - ); - maximumPriority.reverseScreenSpaceError = Math.max( - tile._priorityReverseScreenSpaceError, - maximumPriority.reverseScreenSpaceError - ); - minimumPriority.reverseScreenSpaceError = Math.min( - tile._priorityReverseScreenSpaceError, - minimumPriority.reverseScreenSpaceError - ); -} + if (!isOnScreenLongEnough(tile, frameState)) { + return; + } + + const cameraHasNotStoppedMovingLongEnough = + frameState.camera.timeSinceMoved < tileset.foveatedTimeDelay; + if (tile.priorityDeferred && cameraHasNotStoppedMovingLongEnough) { + return; + } + + tile._requestedFrame = frameState.frameNumber; + tileset._requestedTiles.push(tile); +}; /** * Prevent unnecessary loads while camera is moving by getting the ratio of travel distance to tile size. @@ -300,94 +179,24 @@ function isOnScreenLongEnough(tile, frameState) { } /** - * Add a tile to the list of requested tiles, if appropriate - * - * @private - * @param {Cesium3DTile} tile - * @param {FrameState} frameState - */ -function loadTile(tile, frameState) { - const { tileset } = tile; - if ( - tile._requestedFrame === frameState.frameNumber || - (!hasUnloadedContent(tile) && !tile.contentExpired) - ) { - return; - } - - if (!isOnScreenLongEnough(tile, frameState)) { - return; - } - - const cameraHasNotStoppedMovingLongEnough = - frameState.camera.timeSinceMoved < tileset.foveatedTimeDelay; - if (tile.priorityDeferred && cameraHasNotStoppedMovingLongEnough) { - return; - } - - tile._requestedFrame = frameState.frameNumber; - tileset._requestedTiles.push(tile); -} - -/** - * Wrap Cesium3DTile.prototype.updateVisibility to avoid repeated updates + * Reset some of the tile's flags and re-evaluate visibility and priority * * @private * @param {Cesium3DTile} tile * @param {FrameState} frameState */ -function updateVisibility(tile, frameState) { - const updatedVisibilityFrame = tile.tileset._updatedVisibilityFrame; - if (tile._updatedVisibilityFrame === updatedVisibilityFrame) { - // Return early if visibility has already been checked during the traversal. - // The visibility may have already been checked if the cullWithChildrenBounds optimization is used. - return; - } - - tile.updateVisibility(frameState); - tile._updatedVisibilityFrame = updatedVisibilityFrame; -} - -/** - * @private - * @param {Cesium3DTile} tile - * @param {FrameState} frameState - * @returns {boolean} - */ -function anyChildrenVisible(tile, frameState) { - let anyVisible = false; - const children = tile.children; - for (let i = 0; i < children.length; ++i) { - const child = children[i]; - updateVisibility(child, frameState); - anyVisible = anyVisible || isVisible(child); - } - return anyVisible; -} +Cesium3DTilesetTraversal.updateTile = function (tile, frameState) { + updateTileVisibility(tile, frameState); + tile.updateExpiration(); -/** - * @private - * @param {Cesium3DTile} tile - * @param {FrameState} frameState - * @returns {boolean} - */ -function meetsScreenSpaceErrorEarly(tile, frameState) { - const { parent, tileset } = tile; - if ( - !defined(parent) || - parent.hasTilesetContent || - parent.hasImplicitContent || - parent.refine !== Cesium3DTileRefine.ADD - ) { - return false; - } + tile._wasMinPriorityChild = false; + tile._priorityHolder = tile; + updateMinimumMaximumPriority(tile); - // Use parent's geometric error with child's box to see if the tile already meet the SSE - return ( - tile.getScreenSpaceError(frameState, true) <= - tileset._maximumScreenSpaceError - ); -} + // SkipLOD + tile._shouldSelect = false; + tile._finalResolution = true; +}; /** * @private @@ -395,9 +204,9 @@ function meetsScreenSpaceErrorEarly(tile, frameState) { * @param {FrameState} frameState */ function updateTileVisibility(tile, frameState) { - updateVisibility(tile, frameState); + tile.updateVisibility(frameState); - if (!isVisible(tile)) { + if (!tile.isVisible) { return; } @@ -432,464 +241,81 @@ function updateTileVisibility(tile, frameState) { } /** - * Reset some of the tile's flags and re-evaluate visibility and priority - * - * @private - * @param {Cesium3DTile} tile - * @param {FrameState} frameState - */ -function updateTile(tile, frameState) { - updateTileVisibility(tile, frameState); - tile.updateExpiration(); - - tile._wasMinPriorityChild = false; - tile._priorityHolder = tile; - updateMinimumMaximumPriority(tile); - - // SkipLOD - tile._shouldSelect = false; - tile._finalResolution = true; -} - -/** - * Update links to the ancestor tiles that have content - * * @private * @param {Cesium3DTile} tile * @param {FrameState} frameState - */ -function updateTileAncestorContentLinks(tile, frameState) { - tile._ancestorWithContent = undefined; - tile._ancestorWithContentAvailable = undefined; - - const { parent } = tile; - if (!defined(parent)) { - return; - } - const parentHasContent = - !hasUnloadedContent(parent) || - parent._requestedFrame === frameState.frameNumber; - - // ancestorWithContent is an ancestor that has content or has the potential to have - // content. Used in conjunction with tileset.skipLevels to know when to skip a tile. - tile._ancestorWithContent = parentHasContent - ? parent - : parent._ancestorWithContent; - - // ancestorWithContentAvailable is an ancestor that is rendered if a desired tile is not loaded - tile._ancestorWithContentAvailable = parent.contentAvailable - ? parent - : parent._ancestorWithContentAvailable; -} - -/** - * @private - * @param {Cesium3DTile} tile * @returns {boolean} */ -function hasEmptyContent(tile) { - return ( - tile.hasEmptyContent || tile.hasTilesetContent || tile.hasImplicitContent - ); -} - -/** - * @private - * @param {Cesium3DTile} tile - * @returns {boolean} - */ -function hasUnloadedContent(tile) { - return !hasEmptyContent(tile) && tile.contentUnloaded; -} +function meetsScreenSpaceErrorEarly(tile, frameState) { + const { parent, tileset } = tile; + if ( + !defined(parent) || + parent.hasTilesetContent || + parent.hasImplicitContent || + parent.refine !== Cesium3DTileRefine.ADD + ) { + return false; + } -/** - * Determine if a tile has reached the limit of level of detail skipping. - * If so, it should _not_ be skipped: it should be loaded and rendered - * - * @private - * @param {Cesium3DTileset} tileset - * @param {Cesium3DTile} tile - * @returns {boolean} true if this tile should not be skipped - */ -function reachedSkippingThreshold(tileset, tile) { - const ancestor = tile._ancestorWithContent; + // Use parent's geometric error with child's box to see if the tile already meet the SSE return ( - !tileset.immediatelyLoadDesiredLevelOfDetail && - (tile._priorityProgressiveResolutionScreenSpaceErrorLeaf || - (defined(ancestor) && - tile._screenSpaceError < - ancestor._screenSpaceError / tileset.skipScreenSpaceErrorFactor && - tile._depth > ancestor._depth + tileset.skipLevels)) + tile.getScreenSpaceError(frameState, true) <= + tileset._maximumScreenSpaceError ); } -/** - * Sort by farthest child first since this is going on a stack - * - * @private - * @param {Cesium3DTile} a - * @param {Cesium3DTile} b - * @returns {number} - */ -function sortChildrenByDistanceToCamera(a, b) { - if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { - return b._centerZDepth - a._centerZDepth; - } - - return b._distanceToCamera - a._distanceToCamera; -} - /** * @private * @param {Cesium3DTile} tile - * @param {ManagedArray} stack * @param {FrameState} frameState * @returns {boolean} */ -function updateAndPushChildren(tile, stack, frameState) { - const replace = tile.refine === Cesium3DTileRefine.REPLACE; - const { tileset, children } = tile; - - for (let i = 0; i < children.length; ++i) { - updateTile(children[i], frameState); - } - - // Sort by distance to take advantage of early Z and reduce artifacts for skipLevelOfDetail - children.sort(sortChildrenByDistanceToCamera); - - // For traditional replacement refinement only refine if all children are loaded. - // Empty tiles are exempt since it looks better if children stream in as they are loaded to fill the empty space. - const checkRefines = - !skipLevelOfDetail(tileset) && replace && !hasEmptyContent(tile); - let refines = true; - - let anyChildrenVisible = false; - - // Determining min child - let minIndex = -1; - let minimumPriority = Number.MAX_VALUE; - +function anyChildrenVisible(tile, frameState) { + let anyVisible = false; + const children = tile.children; for (let i = 0; i < children.length; ++i) { const child = children[i]; - if (isVisible(child)) { - stack.push(child); - if (child._foveatedFactor < minimumPriority) { - minIndex = i; - minimumPriority = child._foveatedFactor; - } - anyChildrenVisible = true; - } else if (checkRefines || tileset.loadSiblings) { - // Keep non-visible children loaded since they are still needed before the parent can refine. - // Or loadSiblings is true so always load tiles regardless of visibility. - if (child._foveatedFactor < minimumPriority) { - minIndex = i; - minimumPriority = child._foveatedFactor; - } - loadTile(child, frameState); - touchTile(child, frameState); - } - if (checkRefines) { - let childRefines; - if (!child._inRequestVolume) { - childRefines = false; - } else if (hasEmptyContent(child)) { - childRefines = executeEmptyTraversal(child, frameState); - } else { - childRefines = child.contentAvailable; - } - refines = refines && childRefines; - } + child.updateVisibility(frameState); + anyVisible = anyVisible || child.isVisible; } - - if (!anyChildrenVisible) { - refines = false; - } - - if (minIndex !== -1 && !skipLevelOfDetail(tileset) && replace) { - // An ancestor will hold the _foveatedFactor and _distanceToCamera for descendants between itself and its highest priority descendant. Siblings of a min children along the way use this ancestor as their priority holder as well. - // Priority of all tiles that refer to the _foveatedFactor and _distanceToCamera stored in the common ancestor will be differentiated based on their _depth. - const minPriorityChild = children[minIndex]; - minPriorityChild._wasMinPriorityChild = true; - const priorityHolder = - (tile._wasMinPriorityChild || tile === tileset.root) && - minimumPriority <= tile._priorityHolder._foveatedFactor - ? tile._priorityHolder - : tile; // This is where priority dependency chains are wired up or started anew. - priorityHolder._foveatedFactor = Math.min( - minPriorityChild._foveatedFactor, - priorityHolder._foveatedFactor - ); - priorityHolder._distanceToCamera = Math.min( - minPriorityChild._distanceToCamera, - priorityHolder._distanceToCamera - ); - - for (let i = 0; i < children.length; ++i) { - children[i]._priorityHolder = priorityHolder; - } - } - - return refines; -} - -/** - * Determine if a tile is part of the base traversal. - * If not, this tile could be considered for level of detail skipping - * - * @private - * @param {Cesium3DTile} tile - * @param {number} baseScreenSpaceError - * @returns {boolean} - */ -function inBaseTraversal(tile, baseScreenSpaceError) { - const { tileset } = tile; - if (!skipLevelOfDetail(tileset)) { - return true; - } - if (tileset.immediatelyLoadDesiredLevelOfDetail) { - return false; - } - if (!defined(tile._ancestorWithContent)) { - // Include root or near-root tiles in the base traversal so there is something to select up to - return true; - } - if (tile._screenSpaceError === 0.0) { - // If a leaf, use parent's SSE - return tile.parent._screenSpaceError > baseScreenSpaceError; - } - return tile._screenSpaceError > baseScreenSpaceError; + return anyVisible; } /** - * Determine if a tile can and should be traversed for children tiles that - * would contribute to rendering the current view - * * @private * @param {Cesium3DTile} tile - * @returns {boolean} - */ -function canTraverse(tile) { - if (tile.children.length === 0) { - return false; - } - if (tile.hasTilesetContent || tile.hasImplicitContent) { - // Traverse external tileset to visit its root tile - // Don't traverse if the subtree is expired because it will be destroyed - return !tile.contentExpired; - } - return tile._screenSpaceError > tile.tileset._maximumScreenSpaceError; -} - -/** - * Depth-first traversal that traverses all visible tiles and marks tiles for selection. - * If skipLevelOfDetail is off then a tile does not refine until all children are loaded. - * This is the traditional replacement refinement approach and is called the base traversal. - * Tiles that have a greater screen space error than the base screen space error are part of the base traversal, - * all other tiles are part of the skip traversal. The skip traversal allows for skipping levels of the tree - * and rendering children and parent tiles simultaneously. - * - * @private - * @param {Cesium3DTile} root - * @param {number} baseScreenSpaceError - * @param {FrameState} frameState - */ -function executeTraversal(root, baseScreenSpaceError, frameState) { - const { tileset } = root; - const stack = traversal.stack; - stack.push(root); - - while (stack.length > 0) { - traversal.stackMaximumLength = Math.max( - traversal.stackMaximumLength, - stack.length - ); - - const tile = stack.pop(); - - updateTileAncestorContentLinks(tile, frameState); - const parent = tile.parent; - const parentRefines = !defined(parent) || parent._refines; - - tile._refines = canTraverse(tile) - ? updateAndPushChildren(tile, stack, frameState) && parentRefines - : false; - - const stoppedRefining = !tile._refines && parentRefines; - - if (hasEmptyContent(tile)) { - // Add empty tile just to show its debug bounding volume - // If the tile has tileset content load the external tileset - // If the tile cannot refine further select its nearest loaded ancestor - tileset._emptyTiles.push(tile); - loadTile(tile, frameState); - if (stoppedRefining) { - selectDesiredTile(tile, frameState); - } - } else if (tile.refine === Cesium3DTileRefine.ADD) { - // Additive tiles are always loaded and selected - selectDesiredTile(tile, frameState); - loadTile(tile, frameState); - } else if (tile.refine === Cesium3DTileRefine.REPLACE) { - if (inBaseTraversal(tile, baseScreenSpaceError)) { - // Always load tiles in the base traversal - // Select tiles that can't refine further - loadTile(tile, frameState); - if (stoppedRefining) { - selectDesiredTile(tile, frameState); - } - } else if (stoppedRefining) { - // In skip traversal, load and select tiles that can't refine further - selectDesiredTile(tile, frameState); - loadTile(tile, frameState); - } else if (reachedSkippingThreshold(tileset, tile)) { - // In skip traversal, load tiles that aren't skipped - loadTile(tile, frameState); - } - } - - visitTile(tile, frameState); - touchTile(tile, frameState); - } -} - -/** - * Depth-first traversal that checks if all nearest descendants with content are loaded. - * Ignores visibility. - * - * @private - * @param {Cesium3DTile} root - * @param {FrameState} frameState - * @returns {boolean} - */ -function executeEmptyTraversal(root, frameState) { - let allDescendantsLoaded = true; - const stack = emptyTraversal.stack; - stack.push(root); - - while (stack.length > 0) { - emptyTraversal.stackMaximumLength = Math.max( - emptyTraversal.stackMaximumLength, - stack.length - ); - - const tile = stack.pop(); - const children = tile.children; - const childrenLength = children.length; - - // Only traverse if the tile is empty - traversal stop at descendants with content - const emptyContent = hasEmptyContent(tile); - const traverse = emptyContent && canTraverse(tile); - const emptyLeaf = emptyContent && tile.children.length === 0; - - // Traversal stops but the tile does not have content yet - // There will be holes if the parent tries to refine to its children, so don't refine - // One exception: a parent may refine even if one of its descendants is an empty leaf - if (!traverse && !tile.contentAvailable && !emptyLeaf) { - allDescendantsLoaded = false; - } - - updateTile(tile, frameState); - if (!isVisible(tile)) { - // Load tiles that aren't visible since they are still needed for the parent to refine - loadTile(tile, frameState); - touchTile(tile, frameState); - } - - if (traverse) { - for (let i = 0; i < childrenLength; ++i) { - const child = children[i]; - stack.push(child); - } - } - } - - return allDescendantsLoaded; -} - -/** - * Traverse the tree and check if their selected frame is the current frame. If so, add it to a selection queue. - * This is a preorder traversal so children tiles are selected before ancestor tiles. - * - * The reason for the preorder traversal is so that tiles can easily be marked with their - * selection depth. A tile's _selectionDepth is its depth in the tree where all non-selected tiles are removed. - * This property is important for use in the stencil test because we want to render deeper tiles on top of their - * ancestors. If a tileset is very deep, the depth is unlikely to fit into the stencil buffer. - * - * We want to select children before their ancestors because there is no guarantee on the relationship between - * the children's z-depth and the ancestor's z-depth. We cannot rely on Z because we want the child to appear on top - * of ancestor regardless of true depth. The stencil tests used require children to be drawn first. - * - * NOTE: 3D Tiles uses 3 bits from the stencil buffer meaning this will not work when there is a chain of - * selected tiles that is deeper than 7. This is not very likely. - * - * @private - * @param {Cesium3DTile} root - * @param {FrameState} frameState */ -function traverseAndSelect(root, frameState) { - const { stack, ancestorStack } = selectionTraversal; - let lastAncestor; - - stack.push(root); - - while (stack.length > 0 || ancestorStack.length > 0) { - selectionTraversal.stackMaximumLength = Math.max( - selectionTraversal.stackMaximumLength, - stack.length - ); - selectionTraversal.ancestorStackMaximumLength = Math.max( - selectionTraversal.ancestorStackMaximumLength, - ancestorStack.length - ); - - if (ancestorStack.length > 0) { - const waitingTile = ancestorStack.peek(); - if (waitingTile._stackLength === stack.length) { - ancestorStack.pop(); - if (waitingTile !== lastAncestor) { - waitingTile._finalResolution = false; - } - selectTile(waitingTile, frameState); - continue; - } - } - - const tile = stack.pop(); - if (!defined(tile)) { - // stack is empty but ancestorStack isn't - continue; - } - - const traverse = canTraverse(tile); - - if (tile._shouldSelect) { - if (tile.refine === Cesium3DTileRefine.ADD) { - selectTile(tile, frameState); - } else { - tile._selectionDepth = ancestorStack.length; - if (tile._selectionDepth > 0) { - tile.tileset._hasMixedContent = true; - } - lastAncestor = tile; - if (!traverse) { - selectTile(tile, frameState); - continue; - } - ancestorStack.push(tile); - tile._stackLength = stack.length; - } - } +function updateMinimumMaximumPriority(tile) { + const minimumPriority = tile.tileset._minimumPriority; + const maximumPriority = tile.tileset._maximumPriority; + const priorityHolder = tile._priorityHolder; - if (traverse) { - const children = tile.children; - for (let i = 0; i < children.length; ++i) { - const child = children[i]; - if (isVisible(child)) { - stack.push(child); - } - } - } - } + maximumPriority.distance = Math.max( + priorityHolder._distanceToCamera, + maximumPriority.distance + ); + minimumPriority.distance = Math.min( + priorityHolder._distanceToCamera, + minimumPriority.distance + ); + maximumPriority.depth = Math.max(tile._depth, maximumPriority.depth); + minimumPriority.depth = Math.min(tile._depth, minimumPriority.depth); + maximumPriority.foveatedFactor = Math.max( + priorityHolder._foveatedFactor, + maximumPriority.foveatedFactor + ); + minimumPriority.foveatedFactor = Math.min( + priorityHolder._foveatedFactor, + minimumPriority.foveatedFactor + ); + maximumPriority.reverseScreenSpaceError = Math.max( + tile._priorityReverseScreenSpaceError, + maximumPriority.reverseScreenSpaceError + ); + minimumPriority.reverseScreenSpaceError = Math.min( + tile._priorityReverseScreenSpaceError, + minimumPriority.reverseScreenSpaceError + ); } export default Cesium3DTilesetTraversal; diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index d7fbad7f790e..4b53df8e3740 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -2574,10 +2574,6 @@ Model.prototype.hasSilhouette = function (frameState) { ); }; -function supportsSkipLevelOfDetail(frameState) { - return frameState.context.stencilBuffer; -} - /** * Gets whether or not the model is part of a tileset that uses the * skipLevelOfDetail optimization. This accounts for whether skipLevelOfDetail @@ -2588,13 +2584,13 @@ function supportsSkipLevelOfDetail(frameState) { * @private */ Model.prototype.hasSkipLevelOfDetail = function (frameState) { - const is3DTiles = ModelType.is3DTiles(this.type); - if (!is3DTiles) { + if (!ModelType.is3DTiles(this.type)) { return false; } + const supportsSkipLevelOfDetail = frameState.context.stencilBuffer; const tileset = this._content.tileset; - return supportsSkipLevelOfDetail(frameState) && tileset.skipLevelOfDetail; + return supportsSkipLevelOfDetail && tileset.isSkippingLevelOfDetail; }; /** diff --git a/packages/engine/Source/Scene/Model/ModelDrawCommand.js b/packages/engine/Source/Scene/Model/ModelDrawCommand.js index a82b75fc4b52..83323fc1ec7d 100644 --- a/packages/engine/Source/Scene/Model/ModelDrawCommand.js +++ b/packages/engine/Source/Scene/Model/ModelDrawCommand.js @@ -437,9 +437,8 @@ function updateShadows(drawCommand) { const receiveShadows = ShadowMode.receiveShadows(shadows); const derivedCommands = drawCommand._derivedCommands; - const length = derivedCommands.length; - for (let i = 0; i < length; ++i) { + for (let i = 0; i < derivedCommands.length; ++i) { const derivedCommand = derivedCommands[i]; if (derivedCommand.updateShadows) { const command = derivedCommand.command; @@ -451,11 +450,9 @@ function updateShadows(drawCommand) { function updateBackFaceCulling(drawCommand) { const backFaceCulling = drawCommand.backFaceCulling; - const derivedCommands = drawCommand._derivedCommands; - const length = derivedCommands.length; - for (let i = 0; i < length; ++i) { + for (let i = 0; i < derivedCommands.length; ++i) { const derivedCommand = derivedCommands[i]; if (derivedCommand.updateBackFaceCulling) { const command = derivedCommand.command; @@ -468,11 +465,9 @@ function updateBackFaceCulling(drawCommand) { function updateCullFace(drawCommand) { const cullFace = drawCommand.cullFace; - const derivedCommands = drawCommand._derivedCommands; - const length = derivedCommands.length; - for (let i = 0; i < length; ++i) { + for (let i = 0; i < derivedCommands.length; ++i) { const derivedCommand = derivedCommands[i]; if (derivedCommand.updateCullFace) { const command = derivedCommand.command; @@ -485,11 +480,9 @@ function updateCullFace(drawCommand) { function updateDebugShowBoundingVolume(drawCommand) { const debugShowBoundingVolume = drawCommand.debugShowBoundingVolume; - const derivedCommands = drawCommand._derivedCommands; - const length = derivedCommands.length; - for (let i = 0; i < length; ++i) { + for (let i = 0; i < derivedCommands.length; ++i) { const derivedCommand = derivedCommands[i]; if (derivedCommand.updateDebugShowBoundingVolume) { const command = derivedCommand.command; @@ -537,15 +530,10 @@ ModelDrawCommand.prototype.pushCommands = function (frameState, result) { } if (this._needsSkipLevelOfDetailCommands) { - const content = this._model.content; - const tileset = content.tileset; - const tile = content.tile; - - const hasMixedContent = tileset._hasMixedContent; - const finalResolution = tile._finalResolution; + const { tileset, tile } = this._model.content; - if (hasMixedContent) { - if (!finalResolution) { + if (tileset.hasMixedContent) { + if (!tile._finalResolution) { pushCommand( tileset._backfaceCommands, this._skipLodBackfaceCommand, @@ -846,13 +834,14 @@ function deriveSkipLodStencilCommand(command) { const stencilCommand = DrawCommand.shallowClone(command); const renderState = clone(command.renderState, true); // The stencil reference is updated dynamically; see updateSkipLodStencilCommand(). - renderState.stencilTest.enabled = true; - renderState.stencilTest.mask = StencilConstants.SKIP_LOD_MASK; - renderState.stencilTest.reference = StencilConstants.CESIUM_3D_TILE_MASK; - renderState.stencilTest.frontFunction = StencilFunction.GREATER_OR_EQUAL; - renderState.stencilTest.frontOperation.zPass = StencilOperation.REPLACE; - renderState.stencilTest.backFunction = StencilFunction.GREATER_OR_EQUAL; - renderState.stencilTest.backOperation.zPass = StencilOperation.REPLACE; + const { stencilTest } = renderState; + stencilTest.enabled = true; + stencilTest.mask = StencilConstants.SKIP_LOD_MASK; + stencilTest.reference = StencilConstants.CESIUM_3D_TILE_MASK; + stencilTest.frontFunction = StencilFunction.GREATER_OR_EQUAL; + stencilTest.frontOperation.zPass = StencilOperation.REPLACE; + stencilTest.backFunction = StencilFunction.GREATER_OR_EQUAL; + stencilTest.backOperation.zPass = StencilOperation.REPLACE; renderState.stencilMask = StencilConstants.CESIUM_3D_TILE_MASK | StencilConstants.SKIP_LOD_MASK; diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetBaseTraversalSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetBaseTraversalSpec.js new file mode 100644 index 000000000000..3c4782ee83c0 --- /dev/null +++ b/packages/engine/Specs/Scene/Cesium3DTilesetBaseTraversalSpec.js @@ -0,0 +1,12 @@ +import { + Cesium3DTilesetTraversal, + Cesium3DTilesetBaseTraversal, +} from "../../index.js"; + +describe("Scene/Cesium3DTilesetBaseTraversal", function () { + it("conforms to Cesium3DTilesetTraversal interface", function () { + expect(Cesium3DTilesetBaseTraversal).toConformToInterface( + Cesium3DTilesetTraversal + ); + }); +}); diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetMostDetailedTraversalSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetMostDetailedTraversalSpec.js new file mode 100644 index 000000000000..364c9369df1a --- /dev/null +++ b/packages/engine/Specs/Scene/Cesium3DTilesetMostDetailedTraversalSpec.js @@ -0,0 +1,12 @@ +import { + Cesium3DTilesetTraversal, + Cesium3DTilesetMostDetailedTraversal, +} from "../../index.js"; + +describe("Scene/Cesium3DTilesetMostDetailedTraversal", function () { + it("conforms to Cesium3DTilesetTraversal interface", function () { + expect(Cesium3DTilesetMostDetailedTraversal).toConformToInterface( + Cesium3DTilesetTraversal + ); + }); +}); diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetSkipTraversalSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSkipTraversalSpec.js new file mode 100644 index 000000000000..bb1418cebfcf --- /dev/null +++ b/packages/engine/Specs/Scene/Cesium3DTilesetSkipTraversalSpec.js @@ -0,0 +1,12 @@ +import { + Cesium3DTilesetTraversal, + Cesium3DTilesetSkipTraversal, +} from "../../index.js"; + +describe("Scene/Cesium3DTilesetSkipTraversal", function () { + it("conforms to Cesium3DTilesetTraversal interface", function () { + expect(Cesium3DTilesetSkipTraversal).toConformToInterface( + Cesium3DTilesetTraversal + ); + }); +}); diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js index e93bdc4c7f20..d0535302cc31 100644 --- a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js +++ b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js @@ -3802,12 +3802,12 @@ describe( const statistics = tileset._statistics; expect(statistics.numberOfTilesWithContentReady).toEqual(1); expect(tileset._selectedTiles[0]._selectionDepth).toEqual(0); - expect(tileset._hasMixedContent).toBe(false); + expect(tileset.hasMixedContent).toBe(false); return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then( function (tileset) { expect(statistics.numberOfTilesWithContentReady).toEqual(5); - expect(tileset._hasMixedContent).toBe(false); + expect(tileset.hasMixedContent).toBe(false); } ); }); @@ -3828,7 +3828,7 @@ describe( scene.renderForSpecs(); - expect(tileset._hasMixedContent).toBe(true); + expect(tileset.hasMixedContent).toBe(true); expect(statistics.numberOfTilesWithContentReady).toEqual(2); expect( tileset.root.children[0].children[0].children[3]._selectionDepth @@ -3838,7 +3838,7 @@ describe( return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then( function (tileset) { expect(statistics.numberOfTilesWithContentReady).toEqual(5); - expect(tileset._hasMixedContent).toBe(false); + expect(tileset.hasMixedContent).toBe(false); } ); }); @@ -3889,7 +3889,7 @@ describe( expect(root.children[0].children[0].children[3]._finalResolution).toBe( true ); - expect(tileset._hasMixedContent).toBe(true); + expect(tileset.hasMixedContent).toBe(true); const commandList = scene.frameState.commandList; const rs = commandList[1].renderState; @@ -3933,7 +3933,7 @@ describe( expect( isSelected(tileset, root.children[0].children[0].children[3]) ).toBe(false); - expect(tileset._hasMixedContent).toBe(false); + expect(tileset.hasMixedContent).toBe(false); return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset); }); diff --git a/packages/engine/Specs/Scene/Model/ModelDrawCommandSpec.js b/packages/engine/Specs/Scene/Model/ModelDrawCommandSpec.js index 99a5fb9a7fe3..6e86f1b0f1b6 100644 --- a/packages/engine/Specs/Scene/Model/ModelDrawCommandSpec.js +++ b/packages/engine/Specs/Scene/Model/ModelDrawCommandSpec.js @@ -88,7 +88,7 @@ describe( _projectTo2D: false, content: { tileset: { - _hasMixedContent: true, + hasMixedContent: true, _backfaceCommands: [], }, tile: { diff --git a/travis/coverage.sh b/travis/coverage.sh index d718e9a5d6b4..f4c929ec6cf9 100755 --- a/travis/coverage.sh +++ b/travis/coverage.sh @@ -4,7 +4,7 @@ if [ $TRAVIS_BRANCH != "cesium.com" ]; then npm --silent run build npm --silent run coverage -- --browsers FirefoxHeadless --webgl-stub --failTaskOnError --suppressPassed - if [ $TRAVIS_REPO_SLUG == "CesiumGS/cesium" ]; then + if [ $TRAVIS_SECURE_ENV_VARS ]; then aws s3 sync ./Build/Coverage s3://cesium-dev/cesium/$TRAVIS_BRANCH/Build/Coverage --delete --color on fi fi diff --git a/travis/deploy.sh b/travis/deploy.sh index b1ec01abbaf3..26cab879a9f4 100755 --- a/travis/deploy.sh +++ b/travis/deploy.sh @@ -1,6 +1,6 @@ #!/bin/bash set -ev -if [ $TRAVIS_REPO_SLUG == "CesiumGS/cesium" ]; then +if [ $TRAVIS_SECURE_ENV_VARS ]; then # Files deployed to cesium.com are "production", and should be cached at edge locations for # better performance. Otherwise, this is a development deploy and nothing should be cached if [ $TRAVIS_BRANCH == "cesium.com" ]; then