Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- Fix issues with label background when updating properties while `label.show` is `false`. [#12138](https://github.com/CesiumGS/cesium/issues/12138)
- Improved performance of `scene.drillPick`. [#12916](https://github.com/CesiumGS/cesium/pull/12916)
- Improved performance when removing primitives. [#3018](https://github.com/CesiumGS/cesium/pull/3018)
- Billboards using `imageSubRegion` now render as expected. [#12585](https://github.com/CesiumGS/cesium/issues/12585)
- Improved performance of terrain Quadtree handling of custom data [#12907](https://github.com/CesiumGS/cesium/pull/12907)
- Fixed picking of `GroundPrimitive` with multiple `PolygonGeometry` instances selecting the wrong instance. [#12978](https://github.com/CesiumGS/cesium/pull/12978)
- Fixed a bug where the removal of draped imagery layers did not update the rendered state [#12923](https://github.com/CesiumGS/cesium/issues/12923)
Expand Down
34 changes: 21 additions & 13 deletions packages/engine/Source/Renderer/TextureAtlas.js
Original file line number Diff line number Diff line change
Expand Up @@ -680,13 +680,13 @@ TextureAtlas.prototype.addImage = function (id, image) {
};

/**
* Add a sub-region of an existing atlas image as additional image indices.
* Get a sub-region of an existing atlas image as additional image indices.
* @private
* @param {string} id The identifier of the existing image.
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
* @returns {Promise<number>} A Promise that resolves to the image region index. -1 is returned if resouces are in the process of being destroyed.
* @returns {number | undefined} The existing subRegion index, or undefined if not yet added.
*/
TextureAtlas.prototype.addImageSubRegion = function (id, subRegion) {
TextureAtlas.prototype.getImageSubRegion = function (id, subRegion) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("id", id);
Check.defined("subRegion", subRegion);
Expand All @@ -697,28 +697,36 @@ TextureAtlas.prototype.addImageSubRegion = function (id, subRegion) {
throw new RuntimeError(`image with id "${id}" not found in the atlas.`);
}

const indexPromise = this._indexPromiseById.get(id);
for (const [index, parentIndex] of this._subRegions.entries()) {
if (imageIndex === parentIndex) {
const boundingRegion = this._rectangles[index];
if (boundingRegion.equals(subRegion)) {
// The subregion is already being tracked
Comment on lines 700 to 704
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do note that unlike my previous implementation, if there are many (thousands+) of subregions then this could get pretty slow, since this is checked by all billboards with subRegions every frame. This could be solved by creating a new BidirectionalMap core class (which would store an additional inverse Map), and using it instead of the regular map for this _subRegions map. I don't think this is needed for this bug fix, but is a possible future improvement if this implementation is chosen over my previous one.

return indexPromise.then((resolvedImageIndex) => {
if (resolvedImageIndex === -1) {
// The atlas has been destroyed
return -1;
}

return index;
});
return index;
}
}
}
};

const index = this._nextIndex++;
/**
* Add a sub-region of an existing atlas image as additional image indices.
* @private
* @param {string} id The identifier of the existing image.
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
* @returns {Promise<number>} A Promise that resolves to the image region index. -1 is returned if resouces are in the process of being destroyed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also just return a number now, right? If the image subregion already exists, it just returns the index of the subregion directly.

*/
TextureAtlas.prototype.addImageSubRegion = function (id, subRegion) {
let index = this.getImageSubRegion(id, subRegion);
if (index) {
return index;
}
const imageIndex = this._indexById.get(id);

index = this._nextIndex++;
this._subRegions.set(index, imageIndex);
this._rectangles[index] = subRegion.clone();

const indexPromise = this._indexPromiseById.get(id);
return indexPromise.then((imageIndex) => {
if (imageIndex === -1) {
// The atlas has been destroyed
Expand Down
2 changes: 0 additions & 2 deletions packages/engine/Source/Scene/Billboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,6 @@ function Billboard(options, billboardCollection) {
this._batchIndex = undefined; // Used only by Vector3DTilePoints and BillboardCollection

this._imageTexture = new BillboardTexture(billboardCollection);
this._imageWidth = undefined;
this._imageHeight = undefined;

this._labelDimensions = undefined;
this._labelHorizontalOrigin = undefined;
Expand Down
44 changes: 41 additions & 3 deletions packages/engine/Source/Scene/BillboardTexture.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,15 +251,33 @@ BillboardTexture.prototype.loadImage = async function (id, image) {
* @param {string} id An identifier to detect whether the image already exists in the atlas.
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
*/
BillboardTexture.prototype.addImageSubRegion = async function (id, subRegion) {
BillboardTexture.prototype.addImageSubRegion = function (id, subRegion) {
this._id = id;
this._loadState = BillboardLoadState.LOADING;
this._loadError = undefined;
this._hasSubregion = true;

const atlas = this._billboardCollection.textureAtlas;
const index = atlas.getImageSubRegion(id, subRegion);

if (index) {
this.setImageSubRegion(index, subRegion);
} else {
this.loadImageSubRegion(id, subRegion);
}
Comment on lines +262 to +266
Copy link
Contributor

@mzschwartz5 mzschwartz5 Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (index) {
this.setImageSubRegion(index, subRegion);
} else {
this.loadImageSubRegion(id, subRegion);
}
if (index) {
this.setImageSubRegion(index, subRegion);
return;
}
this.loadImageSubRegion(id, subRegion);

Just a style preference

};

/**
* @see {TextureAtlas#addImageSubRegion}
* @private
* @param {string} id An identifier to detect whether the image already exists in the atlas.
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
* @returns {Promise<number | undefined>}
*/
BillboardTexture.prototype.loadImageSubRegion = async function (id, subRegion) {
let index;
const atlas = this._billboardCollection.textureAtlas;
try {
this._loadState = BillboardLoadState.LOADING;
index = await atlas.addImageSubRegion(id, subRegion);
} catch (error) {
// There was an error loading the referenced image
Expand All @@ -268,6 +286,27 @@ BillboardTexture.prototype.addImageSubRegion = async function (id, subRegion) {
return;
}

if (this._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}

this._loadState = BillboardLoadState.LOADED;

this.setImageSubRegion(index, subRegion);
};

/**
* @see {TextureAtlas#addImageSubRegion}
* @private
* @param {number | undefined} index The resolved index in the {@link TextureAtlas}
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
*/
BillboardTexture.prototype.setImageSubRegion = function (index, subRegion) {
if (index && this._index === index) {
return;
}

if (!defined(index) || index === -1) {
this._loadState = BillboardLoadState.FAILED;
this._index = -1;
Expand All @@ -280,7 +319,6 @@ BillboardTexture.prototype.addImageSubRegion = async function (id, subRegion) {
this._height = subRegion.height;

this._index = index;
this._loadState = BillboardLoadState.LOADED;

this.dirty = true;
};
Expand Down