From 3c400241a1a82ea25539c81966189e93f683c219 Mon Sep 17 00:00:00 2001 From: Don McCurdy <1848368+donmccurdy@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:25:13 -0500 Subject: [PATCH] fix(textureatlas): Allocate texture space less aggressively on resize --- CHANGES.md | 5 +- .../engine/Source/Renderer/TextureAtlas.js | 25 ++-- .../engine/Specs/Renderer/TextureAtlasSpec.js | 116 +++++++++++------- 3 files changed, 81 insertions(+), 65 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a066c4bad50..f0d5d81fd44 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) #### Additions :tada: diff --git a/packages/engine/Source/Renderer/TextureAtlas.js b/packages/engine/Source/Renderer/TextureAtlas.js index ba3575b57e9..f61822f826a 100644 --- a/packages/engine/Source/Renderer/TextureAtlas.js +++ b/packages/engine/Source/Renderer/TextureAtlas.js @@ -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; - - // 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; } } diff --git a/packages/engine/Specs/Renderer/TextureAtlasSpec.js b/packages/engine/Specs/Renderer/TextureAtlasSpec.js index a5026921a73..4faeeb02130 100644 --- a/packages/engine/Specs/Renderer/TextureAtlasSpec.js +++ b/packages/engine/Specs/Renderer/TextureAtlasSpec.js @@ -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................ @@ -771,8 +731,7 @@ describe("Scene/TextureAtlas", function () { 333333333333333322222222220..... 3333333333333333222222222201.... `.trim(), - ); - } + ); let textureCoordinates = atlas.computeTextureCoordinates(index0); expect( @@ -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();