diff --git a/CHANGES.md b/CHANGES.md index d229c66b2ccc..eef88f6ec365 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,9 +4,20 @@ #### @cesium/engine +##### Breaking Changes :mega: + +- The `Cesium3DTileset.dynamicScreenSpaceError` optimization is now enabled by default, as this improves performance for street-level horizon views. Furthermore, the default settings of this feature were tuned for improved performance. `Cesium3DTileset.dynamicScreenSpaceErrorDensity` was changed from 0.00278 to 0.0002. `Cesium3DTileset.dynamicScreenSpaceErrorFactor` was changed from 4 to 24. [#11718](https://github.com/CesiumGS/cesium/pull/11718) + ##### Fixes :wrench: - Fix globe materials when lighting is false. Slope/Aspect material no longer rely on turning on lighting or shadows. [#11563](https://github.com/CesiumGS/cesium/issues/11563) +- Fixed a bug where the `Cesium3DTileset` constructor was ignoring the options `dynamicScreenSpaceError`, `dynamicScreenSpaceErrorDensity`, `dynamicScreenSpaceErrorFactor` and `dynamicScreenSpaceErrorHeightFalloff`. [#11677](https://github.com/CesiumGS/cesium/issues/11677) + +#### @cesium/widgets + +##### Fixes :wrench: + +- Fixed a bug where the 3D Tiles Inspector's `dynamicScreenSpaceErrorDensity` slider did not update the tileset [#6143](https://github.com/CesiumGS/cesium/issues/6143) ### 1.113 - 2024-01-02 diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 0f5f3b56bcb9..09592281a59d 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -17,6 +17,7 @@ import IonResource from "../Core/IonResource.js"; import JulianDate from "../Core/JulianDate.js"; import ManagedArray from "../Core/ManagedArray.js"; import CesiumMath from "../Core/Math.js"; +import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import Resource from "../Core/Resource.js"; import RuntimeError from "../Core/RuntimeError.js"; @@ -75,10 +76,10 @@ import Cesium3DTilesetSkipTraversal from "./Cesium3DTilesetSkipTraversal.js"; * @property {boolean} [preloadWhenHidden=false] Preload tiles when tileset.show is false. Loads tiles as if the tileset is visible but does not render them. * @property {boolean} [preloadFlightDestinations=true] Optimization option. Preload tiles at the camera's flight destination while the camera is in flight. * @property {boolean} [preferLeaves=false] Optimization option. Prefer loading of leaves first. - * @property {boolean} [dynamicScreenSpaceError=false] Optimization option. Reduce the screen space error for tiles that are further away from the camera. - * @property {number} [dynamicScreenSpaceErrorDensity=0.00278] Density used to adjust the dynamic screen space error, similar to fog density. - * @property {number} [dynamicScreenSpaceErrorFactor=4.0] A factor used to increase the computed dynamic screen space error. - * @property {number} [dynamicScreenSpaceErrorHeightFalloff=0.25] A ratio of the tileset's height at which the density starts to falloff. + * @property {boolean} [dynamicScreenSpaceError=true] Optimization option. For street-level horizon views, use lower resolution tiles far from the camera. This reduces the amount of data loaded and improves tileset loading time with a slight drop in visual quality in the distance. + * @property {number} [dynamicScreenSpaceErrorDensity=2.0e-4] Similar to {@link Fog#density}, this option controls the camera distance at which the {@link Cesium3DTileset#dynamicScreenSpaceError} optimization applies. Larger values will cause tiles closer to the camera to be affected. + * @property {number} [dynamicScreenSpaceErrorFactor=24.0] A parameter that controls the intensity of the {@link Cesium3DTileset#dynamicScreenSpaceError} optimization for tiles on the horizon. Larger values cause lower resolution tiles to load, improving runtime performance at a slight reduction of visual quality. + * @property {number} [dynamicScreenSpaceErrorHeightFalloff=0.25] A ratio of the tileset's height that determines where "street level" camera views occur. When the camera is below this height, the {@link Cesium3DTileset#dynamicScreenSpaceError} optimization will have the maximum effect, and it will roll off above this value. * @property {number} [progressiveResolutionHeightFraction=0.3] Optimization option. If between (0.0, 0.5], tiles at or above the screen space error for the reduced screen resolution of progressiveResolutionHeightFraction*screenHeight will be prioritized first. This can help get a quick layer of tiles down while full resolution tiles continue to load. * @property {boolean} [foveatedScreenSpaceError=true] Optimization option. Prioritize loading tiles in the center of the screen by temporarily raising the screen space error for tiles around the edge of the screen. Screen space error returns to normal once all the tiles in the center of the screen as determined by the {@link Cesium3DTileset#foveatedConeSize} are loaded. * @property {number} [foveatedConeSize=0.1] Optimization option. Used when {@link Cesium3DTileset#foveatedScreenSpaceError} is true to control the cone size that determines which tiles are deferred. Tiles that are inside this cone are loaded immediately. Tiles outside the cone are potentially deferred based on how far outside the cone they are and their screen space error. This is controlled by {@link Cesium3DTileset#foveatedInterpolationCallback} and {@link Cesium3DTileset#foveatedMinimumScreenSpaceErrorRelaxation}. Setting this to 0.0 means the cone will be the line formed by the camera position and its view direction. Setting this to 1.0 means the cone encompasses the entire field of view of the camera, disabling the effect. @@ -166,8 +167,8 @@ import Cesium3DTilesetSkipTraversal from "./Cesium3DTilesetSkipTraversal.js"; * const tileset = await Cesium.Cesium3DTileset.fromUrl( * "http://localhost:8002/tilesets/Seattle/tileset.json", { * dynamicScreenSpaceError: true, - * dynamicScreenSpaceErrorDensity: 0.00278, - * dynamicScreenSpaceErrorFactor: 4.0, + * dynamicScreenSpaceErrorDensity: 2.0e-4, + * dynamicScreenSpaceErrorFactor: 24.0, * dynamicScreenSpaceErrorHeightFalloff: 0.25 * }); * scene.primitives.add(tileset); @@ -361,18 +362,18 @@ function Cesium3DTileset(options) { this._pass = undefined; // Cesium3DTilePass /** - * Optimization option. Whether the tileset should refine based on a dynamic screen space error. Tiles that are further - * away will be rendered with lower detail than closer tiles. This improves performance by rendering fewer - * tiles and making less requests, but may result in a slight drop in visual quality for tiles in the distance. - * The algorithm is biased towards "street views" where the camera is close to the ground plane of the tileset and looking - * at the horizon. In addition results are more accurate for tightly fitting bounding volumes like box and region. + * Optimization option. For street-level horizon views, use lower resolution tiles far from the camera. This reduces + * the amount of data loaded and improves tileset loading time with a slight drop in visual quality in the distance. + *

+ * This optimization is strongest when the camera is close to the ground plane of the tileset and looking at the + * horizon. Furthermore, the results are more accurate for tightly fitting bounding volumes like box and region. * * @type {boolean} - * @default false + * @default true */ this.dynamicScreenSpaceError = defaultValue( options.dynamicScreenSpaceError, - false + true ); /** @@ -416,48 +417,68 @@ function Cesium3DTileset(options) { this.foveatedTimeDelay = defaultValue(options.foveatedTimeDelay, 0.2); /** - * A scalar that determines the density used to adjust the dynamic screen space error, similar to {@link Fog}. Increasing this - * value has the effect of increasing the maximum screen space error for all tiles, but in a non-linear fashion. - * The error starts at 0.0 and increases exponentially until a midpoint is reached, and then approaches 1.0 asymptotically. - * This has the effect of keeping high detail in the closer tiles and lower detail in the further tiles, with all tiles - * beyond a certain distance all roughly having an error of 1.0. + * Similar to {@link Fog#density}, this option controls the camera distance at which the {@link Cesium3DTileset#dynamicScreenSpaceError} + * optimization applies. Larger values will cause tiles closer to the camera to be affected. This value must be + * non-negative. + *

+ * This optimization works by rolling off the tile screen space error (SSE) with camera distance like a bell curve. + * This has the effect of selecting lower resolution tiles far from the camera. Near the camera, no adjustment is + * made. For tiles further away, the SSE is reduced by up to {@link Cesium3DTileset#dynamicScreenSpaceErrorFactor} + * (measured in pixels of error). + *

*

- * The dynamic error is in the range [0.0, 1.0) and is multiplied by dynamicScreenSpaceErrorFactor to produce the - * final dynamic error. This dynamic error is then subtracted from the tile's actual screen space error. + * Increasing the density makes the bell curve narrower so tiles closer to the camera are affected. This is analagous + * to moving fog closer to the camera. *

*

- * Increasing dynamicScreenSpaceErrorDensity has the effect of moving the error midpoint closer to the camera. - * It is analogous to moving fog closer to the camera. + * When the density is 0, the optimization will have no effect on the tileset. *

* * @type {number} - * @default 0.00278 + * @default 2.0e-4 */ - this.dynamicScreenSpaceErrorDensity = 0.00278; + this.dynamicScreenSpaceErrorDensity = defaultValue( + options.dynamicScreenSpaceErrorDensity, + 2.0e-4 + ); /** - * A factor used to increase the screen space error of tiles for dynamic screen space error. As this value increases less tiles - * are requested for rendering and tiles in the distance will have lower detail. If set to zero, the feature will be disabled. + * A parameter that controls the intensity of the {@link Cesium3DTileset#dynamicScreenSpaceError} optimization for + * tiles on the horizon. Larger values cause lower resolution tiles to load, improving runtime performance at a slight + * reduction of visual quality. The value must be non-negative. + *

+ * More specifically, this parameter represents the maximum adjustment to screen space error (SSE) in pixels for tiles + * far away from the camera. See {@link Cesium3DTileset#dynamicScreenSpaceErrorDensity} for more details about how + * this optimization works. + *

+ *

+ * When the SSE factor is set to 0, the optimization will have no effect on the tileset. + *

* * @type {number} - * @default 4.0 + * @default 24.0 */ - this.dynamicScreenSpaceErrorFactor = 4.0; + this.dynamicScreenSpaceErrorFactor = defaultValue( + options.dynamicScreenSpaceErrorFactor, + 24.0 + ); /** - * A ratio of the tileset's height at which the density starts to falloff. If the camera is below this height the - * full computed density is applied, otherwise the density falls off. This has the effect of higher density at - * street level views. + * A ratio of the tileset's height that determines "street level" for the {@link Cesium3DTileset#dynamicScreenSpaceError} + * optimization. When the camera is below this height, the dynamic screen space error optimization will have the maximum + * effect, and it will roll off above this value. Valid values are between 0.0 and 1.0. *

- * Valid values are between 0.0 and 1.0. - *

* * @type {number} * @default 0.25 */ - this.dynamicScreenSpaceErrorHeightFalloff = 0.25; + this.dynamicScreenSpaceErrorHeightFalloff = defaultValue( + options.dynamicScreenSpaceErrorHeightFalloff, + 0.25 + ); - this._dynamicScreenSpaceErrorComputedDensity = 0.0; // Updated based on the camera position and direction + // Updated based on the camera position and direction + this._dynamicScreenSpaceErrorComputedDensity = 0.0; /** * Determines whether the tileset casts or receives shadows from light sources. @@ -1968,8 +1989,8 @@ Cesium3DTileset.fromIonAssetId = async function (assetId, options) { * const tileset = await Cesium.Cesium3DTileset.fromUrl( * "http://localhost:8002/tilesets/Seattle/tileset.json", { * dynamicScreenSpaceError: true, - * dynamicScreenSpaceErrorDensity: 0.00278, - * dynamicScreenSpaceErrorFactor: 4.0, + * dynamicScreenSpaceErrorDensity: 2.0e-4, + * dynamicScreenSpaceErrorFactor: 24.0, * dynamicScreenSpaceErrorHeightFalloff: 0.25 * }); * scene.primitives.add(tileset); @@ -2266,6 +2287,7 @@ const scratchMatrix = new Matrix4(); const scratchCenter = new Cartesian3(); const scratchPosition = new Cartesian3(); const scratchDirection = new Cartesian3(); +const scratchHalfHeight = new Cartesian3(); /** * @private @@ -2330,10 +2352,16 @@ function updateDynamicScreenSpaceError(tileset, frameState) { direction = Cartesian3.normalize(direction, direction); height = positionLocal.z; if (tileBoundingVolume instanceof TileOrientedBoundingBox) { - // Assuming z-up, the last component stores the half-height of the box - const boxHeight = root._header.boundingVolume.box[11]; - minimumHeight = centerLocal.z - boxHeight; - maximumHeight = centerLocal.z + boxHeight; + // Assuming z-up, the last column is the local z direction and + // represents the height of the bounding box. + const halfHeightVector = Matrix3.getColumn( + boundingVolume.halfAxes, + 2, + scratchHalfHeight + ); + const halfHeight = Cartesian3.magnitude(halfHeightVector); + minimumHeight = centerLocal.z - halfHeight; + maximumHeight = centerLocal.z + halfHeight; } else if (tileBoundingVolume instanceof TileBoundingSphere) { const radius = boundingVolume.radius; minimumHeight = centerLocal.z - radius; diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js index 2e3381f89d49..d0ba216f7cfd 100644 --- a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js +++ b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js @@ -1096,30 +1096,27 @@ describe( }); }); - function testDynamicScreenSpaceError(url, distance) { - return Cesium3DTilesTester.loadTileset(scene, url).then(function ( - tileset - ) { - const statistics = tileset._statistics; + async function testDynamicScreenSpaceError(url, distance) { + const tileset = await Cesium3DTilesTester.loadTileset(scene, url); + const statistics = tileset._statistics; - // Horizon view, only root is visible - const center = Cartesian3.fromRadians(centerLongitude, centerLatitude); - scene.camera.lookAt(center, new HeadingPitchRange(0.0, 0.0, distance)); + // Horizon view, only root is visible + const center = Cartesian3.fromRadians(centerLongitude, centerLatitude); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, 0.0, distance)); - // Set dynamic SSE to false (default) - tileset.dynamicScreenSpaceError = false; - scene.renderForSpecs(); - expect(statistics.visited).toEqual(1); - expect(statistics.numberOfCommands).toEqual(1); + // Turn off dynamic SSE + tileset.dynamicScreenSpaceError = false; + scene.renderForSpecs(); + expect(statistics.visited).toEqual(1); + expect(statistics.numberOfCommands).toEqual(1); - // Set dynamic SSE to true, now the root is not rendered - tileset.dynamicScreenSpaceError = true; - tileset.dynamicScreenSpaceErrorDensity = 1.0; - tileset.dynamicScreenSpaceErrorFactor = 10.0; - scene.renderForSpecs(); - expect(statistics.visited).toEqual(0); - expect(statistics.numberOfCommands).toEqual(0); - }); + // Turn on dynamic SSE, now the root is not rendered + tileset.dynamicScreenSpaceError = true; + tileset.dynamicScreenSpaceErrorDensity = 1.0; + tileset.dynamicScreenSpaceErrorFactor = 10.0; + scene.renderForSpecs(); + expect(statistics.visited).toEqual(0); + expect(statistics.numberOfCommands).toEqual(0); } function numberOfChildrenWithoutContent(tile) { @@ -1153,6 +1150,39 @@ describe( return testDynamicScreenSpaceError(withTransformSphereUrl, 144.0); }); + it("dynamic screen space error constructor options work", async function () { + const options = { + dynamicScreenSpaceError: true, + dynamicScreenSpaceErrorDensity: 1.0, + dynamicScreenSpaceErrorFactor: 10.0, + dynamicScreenSpaceErrorHeightFalloff: 0.5, + }; + const distance = 103.0; + const tileset = await Cesium3DTilesTester.loadTileset( + scene, + withTransformBoxUrl, + options + ); + + // Make sure the values match the constructor, not hard-coded defaults + // like in https://github.com/CesiumGS/cesium/issues/11677 + expect(tileset.dynamicScreenSpaceError).toBe(true); + expect(tileset.dynamicScreenSpaceErrorDensity).toBe(1.0); + expect(tileset.dynamicScreenSpaceErrorFactor).toBe(10.0); + expect(tileset.dynamicScreenSpaceErrorHeightFalloff).toBe(0.5); + + const statistics = tileset._statistics; + + // Horizon view, only root is in view, however due to dynamic SSE, + // it will not render. + const center = Cartesian3.fromRadians(centerLongitude, centerLatitude); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, 0.0, distance)); + + scene.renderForSpecs(); + expect(statistics.visited).toEqual(0); + expect(statistics.numberOfCommands).toEqual(0); + }); + it("additive refinement - selects root when sse is met", function () { viewRootOnly(); return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function ( diff --git a/packages/engine/Specs/Scene/PickingSpec.js b/packages/engine/Specs/Scene/PickingSpec.js index 4474051a0adc..19cb2c659667 100644 --- a/packages/engine/Specs/Scene/PickingSpec.js +++ b/packages/engine/Specs/Scene/PickingSpec.js @@ -134,9 +134,16 @@ describe( function createTileset(url) { const options = { maximumScreenSpaceError: 0, + // Dynamic screen space error seems to cause a race condition in + // waitForTilesLoaded. + // See https://github.com/CesiumGS/cesium/issues/11732 + dynamicScreenSpaceError: false, }; return Cesium3DTilesTester.loadTileset(scene, url, options).then( function (tileset) { + // The tilesets used in these tests have transforms that are not + // what we want for our camera setup. Re-position the tileset + // in view of the camera const cartographic = Rectangle.center(largeRectangle); const cartesian = Cartographic.toCartesian(cartographic); tileset.root.transform = Matrix4.IDENTITY; diff --git a/packages/widgets/Source/Cesium3DTilesInspector/Cesium3DTilesInspector.js b/packages/widgets/Source/Cesium3DTilesInspector/Cesium3DTilesInspector.js index a88bb400ae96..742434dc7361 100644 --- a/packages/widgets/Source/Cesium3DTilesInspector/Cesium3DTilesInspector.js +++ b/packages/widgets/Source/Cesium3DTilesInspector/Cesium3DTilesInspector.js @@ -215,7 +215,7 @@ function Cesium3DTilesInspector(container, scene) { "Screen Space Error Factor", "dynamicScreenSpaceErrorFactor", 1, - 10, + 32, 0.1 ) ); diff --git a/packages/widgets/Source/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js b/packages/widgets/Source/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js index 42ec352db523..093545e1572b 100644 --- a/packages/widgets/Source/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js +++ b/packages/widgets/Source/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js @@ -722,9 +722,9 @@ function Cesium3DTilesInspectorViewModel(scene, performanceContainer) { * Gets or sets the dynamic screen space error density. This property is observable. * * @type {number} - * @default 0.00278 + * @default 2.0e-4 */ - this.dynamicScreenSpaceErrorDensity = 0.00278; + this.dynamicScreenSpaceErrorDensity = 2.0e-4; /** * Gets or sets the dynamic screen space error density slider value. @@ -732,7 +732,7 @@ function Cesium3DTilesInspectorViewModel(scene, performanceContainer) { * This property is observable. * * @type {number} - * @default 0.00278 + * @default 2.0e-4 */ this.dynamicScreenSpaceErrorDensitySliderValue = undefined; knockout.defineProperty(this, "dynamicScreenSpaceErrorDensitySliderValue", { @@ -740,7 +740,11 @@ function Cesium3DTilesInspectorViewModel(scene, performanceContainer) { return Math.pow(dynamicScreenSpaceErrorDensity(), 1 / 6); }, set: function (value) { - dynamicScreenSpaceErrorDensity(Math.pow(value, 6)); + const scaledValue = Math.pow(value, 6); + dynamicScreenSpaceErrorDensity(scaledValue); + if (defined(that._tileset)) { + that._tileset.dynamicScreenSpaceErrorDensity = scaledValue; + } }, }); @@ -763,9 +767,9 @@ function Cesium3DTilesInspectorViewModel(scene, performanceContainer) { * Gets or sets the dynamic screen space error factor. This property is observable. * * @type {number} - * @default 4.0 + * @default 24.0 */ - this.dynamicScreenSpaceErrorFactor = 4.0; + this.dynamicScreenSpaceErrorFactor = 24.0; const pickTileset = getPickTileset(this); const pickActive = knockout.observable(); diff --git a/packages/widgets/Specs/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js b/packages/widgets/Specs/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js index d681f7cba70a..51267ff50f61 100644 --- a/packages/widgets/Specs/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js +++ b/packages/widgets/Specs/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js @@ -1,4 +1,9 @@ -import { Cesium3DTileset, Cesium3DTileStyle, Globe } from "@cesium/engine"; +import { + Cesium3DTileset, + Cesium3DTileStyle, + Globe, + Math as CesiumMath, +} from "@cesium/engine"; import { Cesium3DTilesInspectorViewModel } from "../../index.js"; import createScene from "../../../../Specs/createScene.js"; @@ -255,6 +260,23 @@ describe( expect(viewModel.tileset.dynamicScreenSpaceErrorFactor).toBe(2); expect(viewModel.tileset.dynamicScreenSpaceErrorDensity).toBe(0.1); }); + + it("dynamicScreenSpaceErrorDensity slider uses an exponential scale", function () { + // The HTML slider produces a linear range, but the actual density value + // varies exponentially. + const rawSliderValue = 0.2; + const scaledValue = Math.pow(rawSliderValue, 6); + + viewModel.dynamicScreenSpaceErrorDensitySliderValue = rawSliderValue; + expect( + viewModel.dynamicScreenSpaceErrorDensitySliderValue + ).toEqualEpsilon(rawSliderValue, CesiumMath.EPSILON8); + + expect(viewModel.tileset.dynamicScreenSpaceErrorDensity).toEqualEpsilon( + scaledValue, + CesiumMath.EPSILON8 + ); + }); }); describe("style options", function () {