Skip to content
Merged
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
5 changes: 3 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

#### Fixes :wrench:

- Improved scaling of SVGs in billboards [#13020](https://github.com/CesiumGS/cesium/issues/13020)
- Improved scaling of SVGs in billboards [#13020](https://github.com/CesiumGS/cesium/pull/13020)
- Billboards using `imageSubRegion` now render as expected. [#12585](https://github.com/CesiumGS/cesium/issues/12585)
- Fixed depth testing bug with billboards and labels clipping through models [#13012](https://github.com/CesiumGS/cesium/issues/13012)
- Fixed unexpected outline artifacts around billboards [#13038](https://github.com/CesiumGS/cesium/issues/13038)
- Fixed unexpected outline artifacts around billboards [#4525](https://github.com/CesiumGS/cesium/issues/4525)
- Fix texture coordinates in large billboard collections [#13042](https://github.com/CesiumGS/cesium/pull/13042)
Copy link
Contributor

Choose a reason for hiding this comment

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

@donmccurdy sorry I missed this earlier: this changelog entry should have been under a new heading for the January release (1.137).
I can fix it in the next PR.


#### Additions :tada:

Expand Down
25 changes: 8 additions & 17 deletions packages/engine/Source/Renderer/TextureAtlas.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,26 +364,17 @@ TextureAtlas.prototype._resize = function (context, queueOffset = 0) {
toPack.push(queue[i]);
}

// At minimum, the texture will need to scale to accommodate the largest width and height
width = Math.max(maxWidth, width);
height = Math.max(maxHeight, height);
// At minimum, atlas must fit its largest input images. Texture coordinates are
// compressed to 0–1 with 12-bit precision, so use power-of-two size to align pixels.
width = CesiumMath.nextPowerOfTwo(Math.max(maxWidth, width));
height = CesiumMath.nextPowerOfTwo(Math.max(maxHeight, height));

if (!context.webgl2) {
width = CesiumMath.nextPowerOfTwo(width);
height = CesiumMath.nextPowerOfTwo(height);
}

// Determine by what factor the texture need to be scaled by at minimum
const areaDifference = areaQueued;
let scalingFactor = 1.0;
while (areaDifference / width / height >= 1.0) {
scalingFactor *= 2.0;
Copy link
Member Author

@donmccurdy donmccurdy Nov 21, 2025

Choose a reason for hiding this comment

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

In practice this loop can't spend more than 28 iterations for the largest supportable atlas size, because we'll have ... ahem, other challenges ... if the texture atlas size exceeds 16K x 16K. No need to use an increasing scaling factor (which may cause us to skip smaller qualifying atlas sizes) to reduce the number of iterations.


// Resize by one dimension
// Iteratively double the smallest dimension until atlas area is (approximately) sufficient.
while (areaQueued >= width * height) {
if (width > height) {
height *= scalingFactor;
height *= 2;
} else {
width *= scalingFactor;
width *= 2;
}
}

Expand Down
116 changes: 70 additions & 46 deletions packages/engine/Specs/Renderer/TextureAtlasSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -704,56 +704,16 @@ describe("Scene/TextureAtlas", function () {
expect(index2).toEqual(2);
expect(index3).toEqual(3);

// Webgl1 textures should only be powers of 2
const isWebGL2 = scene.frameState.context.webgl2;
const textureWidth = isWebGL2 ? 20 : 32;
const textureHeight = isWebGL2 ? 32 : 16;
const textureWidth = 32;
const textureHeight = 16;

const texture = atlas.texture;
expect(texture.pixelFormat).toEqual(PixelFormat.RGBA);
expect(texture.width).toEqual(textureWidth);
expect(texture.height).toEqual(textureHeight);

if (isWebGL2) {
expect(drawAtlas(atlas, [index0, index1, index2, index3])).toBe(
`
....................
....................
....................
....................
....................
....................
2222222222..........
2222222222..........
2222222222..........
2222222222..........
2222222222..........
2222222222..........
2222222222..........
2222222222..........
2222222222..........
2222222222..........
3333333333333333....
3333333333333333....
3333333333333333....
3333333333333333....
3333333333333333....
3333333333333333....
3333333333333333....
3333333333333333....
3333333333333333....
3333333333333333....
3333333333333333....
3333333333333333....
33333333333333330...
33333333333333330...
33333333333333330...
333333333333333301..
`.trim(),
);
} else {
expect(drawAtlas(atlas, [index0, index1, index2, index3])).toBe(
`
expect(drawAtlas(atlas, [index0, index1, index2, index3])).toBe(
`
3333333333333333................
3333333333333333................
3333333333333333................
Expand All @@ -771,8 +731,7 @@ describe("Scene/TextureAtlas", function () {
333333333333333322222222220.....
3333333333333333222222222201....
`.trim(),
);
}
);

let textureCoordinates = atlas.computeTextureCoordinates(index0);
expect(
Expand Down Expand Up @@ -1456,6 +1415,71 @@ describe("Scene/TextureAtlas", function () {
expect(guid1).not.toEqual(guid2);
});

it("allocates appropriate space on resize", async function () {
const imageWidth = 128;
const imageHeight = 64;

await addImages(25);
let inputPixels = 25 * imageWidth * imageHeight;
let atlasWidth = atlas.texture.width;
let atlasHeight = atlas.texture.height;

// Allocate enough space, but not >>2x more. Aspect ratio should be 1:1, 1:2, or 2:1.
expect(atlasWidth * atlasHeight).toBeGreaterThan(inputPixels);
expect(atlasWidth * atlasHeight).toBeLessThanOrEqual(inputPixels * 3);
expect(atlasWidth / atlasHeight).toBeGreaterThanOrEqual(0.5);
expect(atlasWidth / atlasHeight).toBeLessThanOrEqual(2.0);

await addImages(75);
inputPixels = 75 * imageWidth * imageHeight;
atlasWidth = atlas.texture.width;
atlasHeight = atlas.texture.height;

expect(atlasWidth * atlasHeight).toBeGreaterThan(inputPixels);
expect(atlasWidth * atlasHeight).toBeLessThanOrEqual(inputPixels * 3);
expect(atlasWidth / atlasHeight).toBeGreaterThanOrEqual(0.5);
expect(atlasWidth / atlasHeight).toBeLessThanOrEqual(2.0);

await addImages(256);
inputPixels = 256 * imageWidth * imageHeight;
atlasWidth = atlas.texture.width;
atlasHeight = atlas.texture.height;

expect(atlasWidth * atlasHeight).toBeGreaterThan(inputPixels);
expect(atlasWidth * atlasHeight).toBeLessThanOrEqual(inputPixels * 3);
expect(atlasWidth / atlasHeight).toBeGreaterThanOrEqual(0.5);
expect(atlasWidth / atlasHeight).toBeLessThanOrEqual(2.0);

async function addImages(count) {
atlas = new TextureAtlas();

const imageUrl = createImageDataURL(imageWidth, imageHeight);

const promises = [];
for (let i = 0; i < count; i++) {
promises.push(atlas.addImage(i.toString(), imageUrl));
}

await pollWhilePromise(Promise.all(promises), () => {
atlas.update(scene.frameState.context);
});

return count * imageWidth * imageHeight;
}

function createImageDataURL(width, height) {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;

const ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(0, 0, width, height);

return canvas.toDataURL();
}
});

it("destroys successfully while image is queued", async function () {
atlas = new TextureAtlas();

Expand Down