Merge pull request #12743 from CesiumGS/update-draped-imagery-visibility

Update draped imagery visibility
This commit is contained in:
Gabby Getz 2025-07-21 14:57:10 +00:00 committed by GitHub
commit 7a9df5014d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 229 additions and 78 deletions

View File

@ -6,10 +6,11 @@
#### Fixes :wrench:
- Fixes material flashing when changing properties [#1640](https://github.com/CesiumGS/cesium/issues/1640), [#12716](https://github.com/CesiumGS/cesium/issues/12716)
- Fixed an issue where draped imagery on tilesets was not updated based on the visibility of the imagery layer [#12742](https://github.com/CesiumGS/cesium/issues/12742)
- Fixes an exception when removing a Gaussian splat tileset from the scene primitives when it has more than one tile. [#12726](https://github.com/CesiumGS/cesium/pull/12726)
- Fixes rendering of Gaussian splats when they are scaled by the glTF transform, tileset transform, or model matrix. [#12721](https://github.com/CesiumGS/cesium/issues/12721), [#12718](https://github.com/CesiumGS/cesium/issues/12718)
- Updated the type of many properties and functions of `Scene` to clarify that they may be `undefined`. For the full list check PR: [#12736](https://github.com/CesiumGS/cesium/pull/12736)
- Fixes material flashing when changing properties. [#1640](https://github.com/CesiumGS/cesium/issues/1640), [12716](https://github.com/CesiumGS/cesium/issues/12716)
#### Additions :tada:

View File

@ -12,6 +12,7 @@
*/
class ImageryConfiguration {
constructor(imageryLayer) {
this.show = imageryLayer.show;
this.alpha = imageryLayer.alpha;
this.brightness = imageryLayer.brightness;
this.contrast = imageryLayer.contrast;

View File

@ -109,7 +109,9 @@ class ImageryPipelineStage {
);
// This can happen when none of the imagery textures could
// be obtained, because they had all been INVALID/FAILED.
// be obtained, because they had all been INVALID/FAILED,
// or when none of the imagery layers is actually visible
// according to `show==true`
// Bail out in this case
if (imageryInputs.length === 0) {
return;
@ -758,7 +760,8 @@ class ImageryPipelineStage {
* pipeline stage for draping the given imagery layers over the primitive
* that is described by the given model primitive imagery.
*
* This will obtain the <code>ImageryCoverage</code> objects that are provided by
* For each imagery layer that is currently visible (as of `show==true`), this
* will obtain the <code>ImageryCoverage</code> objects that are provided by
* the given model primitive imagery (and that describe the imagery tiles
* that are covered by the primitive), and create one <code>ImageryInput</code> for
* each of them.
@ -791,6 +794,9 @@ class ImageryPipelineStage {
for (let i = 0; i < imageryLayers.length; i++) {
const imageryLayer = imageryLayers.get(i);
if (!imageryLayer.show) {
continue;
}
const imageryTexCoordAttributeSetIndex =
imageryTexCoordAttributeSetIndices[i];
const mappedPositions =

View File

@ -335,6 +335,9 @@ class ModelImagery {
const imageryLayer = imageryLayers.get(i);
const imageryConfiguration = imageryConfigurations[i];
if (imageryLayer.show !== imageryConfiguration.show) {
return true;
}
if (imageryLayer.alpha !== imageryConfiguration.alpha) {
return true;
}

View File

@ -0,0 +1,108 @@
import {
defined,
ResourceCache,
ShaderBuilder,
ImageryPipelineStage,
} from "../../../index.js";
import createScene from "../../../../../Specs/createScene.js";
import loadTilesetWithImagery from "./loadTilesetWithImagery.js";
describe("Scene/Model/ImageryPipelineStage", function () {
let scene;
beforeAll(function () {
scene = createScene();
});
afterAll(function () {
scene.destroyForSpecs();
});
afterEach(function () {
scene.primitives.removeAll();
ResourceCache.clearForSpecs();
});
// A mock `FrameState` object, extracted from other specs, for
// the ImageryPipelineStage.process call
const mockFrameState = {
context: {
defaultTexture: {},
defaultNormalTexture: {},
defaultEmissiveTexture: {},
},
};
// Create a mock `PrimitiveRenderResources` object, extracted from
// other specs, for the ImageryPipelineStage.process call
function mockRenderResources(model, primitive) {
const count = defined(primitive.indices)
? primitive.indices.count
: primitive.attributes[0].count;
return {
attributes: [],
shaderBuilder: new ShaderBuilder(),
attributeIndex: 1,
count: count,
model: model,
runtimeNode: {
node: {},
},
uniformMap: {},
runtimePrimitive: {},
};
}
it("does not create imagery inputs for imagery layers that are not shown", async function () {
// The prerequisites for computing the imagery inputs are not
// met without a GL context: The "mappedPositions" cannot be
// computed without fetching the primitive POSITION data from
// the GPU
if (!scene.context.webgl2) {
return;
}
// Part of a "regression test" test against https://github.com/CesiumGS/cesium/issues/12742
const tileset = await loadTilesetWithImagery(scene);
// Drill a hole through the loaded structures and create some
// mock data for the ImageryPipelineStage.process call
const imageryLayers = tileset.imageryLayers;
const root = tileset.root;
const content = root.content;
const model = content._model;
const modelImagery = model._modelImagery;
const modelPrimitiveImagery = modelImagery._modelPrimitiveImageries[0];
const primitive = model.sceneGraph.components.nodes[0].primitives[0];
const primitiveRenderResources = mockRenderResources(model, primitive);
const imageryTexCoordAttributeSetIndices = [0];
const frameState = mockFrameState;
ImageryPipelineStage.process(
primitiveRenderResources,
primitive,
frameState,
);
// Initially, the imagery layers are visible, as of show===true,
// and there should be at least one ImageryInput be generated
// to be sent to the shader
const imageryInputsA = ImageryPipelineStage._createImageryInputs(
imageryLayers,
modelPrimitiveImagery,
imageryTexCoordAttributeSetIndices,
);
expect(imageryInputsA.length).not.toBe(0);
// For specs: Hide the imagery layer by setting show = false
imageryLayers.get(0).show = false;
// No more ImageryInput objects should be generated now
const imageryInputsB = ImageryPipelineStage._createImageryInputs(
imageryLayers,
modelPrimitiveImagery,
imageryTexCoordAttributeSetIndices,
);
expect(imageryInputsB.length).toBe(0);
});
});

View File

@ -1,76 +1,7 @@
import {
Cartesian3,
ResourceCache,
Transforms,
ModelImagery,
ImageryLayer,
TileCoordinatesImageryProvider,
HeadingPitchRoll,
WebMercatorTilingScheme,
} from "../../../index.js";
import { ResourceCache, ModelImagery } from "../../../index.js";
import createScene from "../../../../../Specs/createScene.js";
import pollToPromise from "../../../../../Specs/pollToPromise.js";
import Cesium3DTilesTester from "../../../../../Specs/Cesium3DTilesTester.js";
const tileset_unitSquare_fourPrimitives_plain_url =
"./Data/Models/glTF-2.0/unitSquare/tileset_unitSquare_fourPrimitives_plain.json";
/**
* Wait until the root tile of the given tileset is loaded
*
* @param {Cesium3DTileset} tileset The tileset
* @param {Scene} scene The scene
*/
async function waitForRootLoaded(tileset, scene) {
scene.renderForSpecs();
const root = tileset.root;
await pollToPromise(() => {
scene.renderForSpecs();
return root.contentFailed || root.contentReady;
});
}
/**
* Load and return a test tileset that defines an imagery layer,
* waiting until the root of that tileset is loaded.
*
* This means that the resulting <code>tileset.root.content._model._modelImagery</code>
* (including the <code>ModelPrimitiveImagery</code> instances) will be defined and ready.
*
* @param {Scene} scene The scene
* @returns {Cesium3DTileset} The tileset
*/
async function loadTestTilesetWithImagery(scene) {
const url = tileset_unitSquare_fourPrimitives_plain_url;
const tileset = await Cesium3DTilesTester.loadTileset(scene, url);
// Create a non-trivial transform for the tileset
const transform = Transforms.eastNorthUpToFixedFrame(
Cartesian3.fromDegrees(-120.0, 40.0, 1.0),
);
tileset.modelMatrix = transform;
// Set a view that fully shows the tile content
// (a unit square at the position given above)
scene.camera.setView({
destination: new Cartesian3(
-2446354.452726738,
-4237211.248955036,
4077988.0921552004,
),
orientation: new HeadingPitchRoll(Math.PI * 2, -Math.PI / 2, 0),
});
const imageryProvider = new TileCoordinatesImageryProvider({
tilingScheme: new WebMercatorTilingScheme(),
});
const imageryLayer = new ImageryLayer(imageryProvider);
tileset.imageryLayers.add(imageryLayer);
await waitForRootLoaded(tileset, scene);
return tileset;
}
import loadTilesetWithImagery from "./loadTilesetWithImagery.js";
describe("Scene/Model/ModelImagery", function () {
let scene;
@ -96,7 +27,7 @@ describe("Scene/Model/ModelImagery", function () {
});
it("properly reports _hasImagery", async function () {
const tileset = await loadTestTilesetWithImagery(scene);
const tileset = await loadTilesetWithImagery(scene);
const root = tileset.root;
const content = root.content;
@ -114,7 +45,7 @@ describe("Scene/Model/ModelImagery", function () {
});
it("properly reports _allImageryLayersReady", async function () {
const tileset = await loadTestTilesetWithImagery(scene);
const tileset = await loadTilesetWithImagery(scene);
const root = tileset.root;
const content = root.content;
@ -137,7 +68,7 @@ describe("Scene/Model/ModelImagery", function () {
return;
}
const tileset = await loadTestTilesetWithImagery(scene);
const tileset = await loadTilesetWithImagery(scene);
const root = tileset.root;
const content = root.content;
@ -161,8 +92,37 @@ describe("Scene/Model/ModelImagery", function () {
expect(modelImagery._imageryConfigurationsModified()).toBeFalse();
});
it("considers the show flag as part of the imageryConfigurations", async function () {
if (!scene.context.webgl2) {
return;
}
const tileset = await loadTilesetWithImagery(scene);
const root = tileset.root;
const content = root.content;
const model = content._model;
const modelImagery = model._modelImagery;
const imageryLayer = tileset.imageryLayers.get(0);
// Initially, _imageryConfigurationsModified is false (it was just updated)
expect(modelImagery._imageryConfigurationsModified()).toBeFalse();
// For spec: Modify imagery configuration
imageryLayer.show = false;
// Now, _imageryConfigurationsModified is true
expect(modelImagery._imageryConfigurationsModified()).toBeTrue();
// Trigger an update
modelImagery._checkForModifiedImageryConfigurations();
// Now, _imageryConfigurationsModified is false again
expect(modelImagery._imageryConfigurationsModified()).toBeFalse();
});
it("creates one ModelPrimitiveImagery for each primitive", async function () {
const tileset = await loadTestTilesetWithImagery(scene);
const tileset = await loadTilesetWithImagery(scene);
const root = tileset.root;
const content = root.content;

View File

@ -0,0 +1,72 @@
import {
Cartesian3,
HeadingPitchRoll,
ImageryLayer,
TileCoordinatesImageryProvider,
Transforms,
WebMercatorTilingScheme,
} from "../../../index.js";
import pollToPromise from "../../../../../Specs/pollToPromise";
import Cesium3DTilesTester from "../../../../../Specs/Cesium3DTilesTester.js";
// A currently hard-wired tileset to be loaded for imagery draping tests
const tileset_unitSquare_fourPrimitives_plain_url =
"./Data/Models/glTF-2.0/unitSquare/tileset_unitSquare_fourPrimitives_plain.json";
/**
* Wait until the root tile of the given tileset is loaded
*
* @param {Cesium3DTileset} tileset The tileset
* @param {Scene} scene The scene
*/
async function waitForRootLoaded(tileset, scene) {
scene.renderForSpecs();
const root = tileset.root;
await pollToPromise(() => {
scene.renderForSpecs();
return root.contentFailed || root.contentReady;
});
}
/**
* Load and return a test tileset that defines an imagery layer,
* waiting until the root of that tileset is loaded.
*
* This means that the resulting <code>tileset.root.content._model._modelImagery</code>
* (including the <code>ModelPrimitiveImagery</code> instances) will be defined and ready.
*
* @param {Scene} scene The scene
* @returns {Cesium3DTileset} The tileset
*/
async function loadTilesetWithImagery(scene) {
const url = tileset_unitSquare_fourPrimitives_plain_url;
const tileset = await Cesium3DTilesTester.loadTileset(scene, url);
// Create a non-trivial transform for the tileset
const transform = Transforms.eastNorthUpToFixedFrame(
Cartesian3.fromDegrees(-120.0, 40.0, 1.0),
);
tileset.modelMatrix = transform;
// Set a view that fully shows the tile content
// (a unit square at the position given above)
scene.camera.setView({
destination: new Cartesian3(
-2446354.452726738,
-4237211.248955036,
4077988.0921552004,
),
orientation: new HeadingPitchRoll(Math.PI * 2, -Math.PI / 2, 0),
});
const imageryProvider = new TileCoordinatesImageryProvider({
tilingScheme: new WebMercatorTilingScheme(),
});
const imageryLayer = new ImageryLayer(imageryProvider);
tileset.imageryLayers.add(imageryLayer);
await waitForRootLoaded(tileset, scene);
return tileset;
}
export default loadTilesetWithImagery;