Skip to content

Commit

Permalink
WIP on #1650
Browse files Browse the repository at this point in the history
The goal here is to eliminate the half-pixel correction that we used before to
close the seams between imagery tile textures packed into the atlas. see 4e8f16b

While it does prevent the colors from bleeding into the neighboring texture,
it distorts the image a little bit - this is especially visible when zoomed in.

Instead, we'll do this:
- Just add 1px of padding to everything packed into the atlases
- For imagery tiles, duplicate the edge data into this 1px padding, so when the tiles
  are sampled, they blend with the padding, not the neighboring texture.

I tried it and it works ok - but my method of filling the padding is inefficient.
I'm just calling `texSubImage2D` a bunch of times with the full image.

Will followup this commit with something more efficient that only writes the
pixels we need.
  • Loading branch information
bhousel committed Jan 4, 2025
1 parent ab327f2 commit 3bd4723
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 100 deletions.
14 changes: 7 additions & 7 deletions modules/pixi/PixiTextures.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ export class PixiTextures {
return tdata.texture;
}

const padding = atlasID === 'symbol' ? 1 : 0;
const texture = atlas.allocate(width, height, padding, asset);
const avoidSeams = (atlasID === 'tile');
const texture = atlas.allocate(width, height, asset, avoidSeams);
if (!texture) {
throw new Error(`Couldn't allocate texture ${key}`);
}
Expand All @@ -200,11 +200,11 @@ export class PixiTextures {
// But we also want to prevent their colors from spilling into an adjacent tile in the atlas.
// Shrink texture coords by half pixel to avoid this.
// https://gamedev.stackexchange.com/a/49585
if (atlasID === 'tile') {
const rect = texture.frame.clone().pad(-0.5);
texture.frame = rect; // `.frame` setter will call updateUvs() automatically
texture.update(); // maybe not in pixi v8? I'm still seeing tile seams?
}
// if (atlasID === 'tile') {
// const rect = texture.frame.clone().pad(-0.5);
// texture.frame = rect; // `.frame` setter will call updateUvs() automatically
// texture.update(); // maybe not in pixi v8? I'm still seeing tile seams?
// }

this._textureData.set(key, { texture: texture, refcount: 1 });

Expand Down
178 changes: 85 additions & 93 deletions modules/pixi/lib/AtlasAllocator.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ export class AtlasAllocator {
*
* @param {number} width - The width of the requested texture.
* @param {number} height - The height of the requested texture.
* @param {number} padding - The padding requested around the texture, to prevent bleeding.
* @return {PIXI.Texture} The allocated texture, if successful; otherwise, `null`.
* @throws When dimensions are too large to fit on a slab
*/
_allocateTexture(width, height, padding = 0) {
_allocateTexture(width, height) {
// Always include an extra pixel of padding to avoid bleeding into neighbor texture.
// If `avoidSeams=true` we will write pixel data into this space - see Rapid#1650
const padding = 1;

// Cannot allocate a texture larger than the slab size.
if ((width + (2 * padding)) > this.size || (height + (2 * padding)) > this.size) {
throw new Error(`Texture can not exceed slab size of ${this.size}x${this.size}`);
Expand All @@ -46,10 +49,8 @@ export class AtlasAllocator {
if (texture) return texture;
}

// Need another new slab.
// Need another slab.
const slab = new AtlasSource(this.label, this.size);

// Add this slab to the end of the list.
this.slabs.push(slab);

// Issue the texture from this blank slab.
Expand All @@ -66,11 +67,11 @@ export class AtlasAllocator {
* @param {number} padding - Padding required around the texture.
* @return {PIXI.Texture} The issued texture, if successful; otherwise, `null`.
*/
_issueTexture(slab, width, height, padding = 0) {
_issueTexture(slab, width, height, padding = 1) {
const rect = slab._binPacker.allocate(width + (2 * padding), height + (2 * padding));
if (!rect) return null;

rect.pad(-padding); // actual frame shouldn't include the padding
rect.pad(-padding); // The actual frame shouldn't include the padding

const texture = new PIXI.Texture({
source: slab,
Expand All @@ -88,12 +89,12 @@ export class AtlasAllocator {
*
* @param {number} width
* @param {number} height
* @param {number} padding
* @param {*} asset
* @param {boolean} avoidSeams - if true, upon upload we'll fill the padding with pixel data
* @return {PIXI.Texture} The issued texture
* @throws If asset type is unrecognized, or dimensions will not fit on a slab
*/
allocate(width, height, padding, asset) {
allocate(width, height, asset, avoidSeams) {
if (!(asset instanceof HTMLImageElement ||
asset instanceof HTMLCanvasElement ||
asset instanceof ImageBitmap ||
Expand All @@ -103,33 +104,23 @@ export class AtlasAllocator {
throw new Error('Unsupported asset type');
}

const texture = this._allocateTexture(width, height, padding);
if (asset instanceof HTMLImageElement && !asset.complete) {
throw new Error('HTMLImageElement not loaded - allocate in onload handler instead');
}

const texture = this._allocateTexture(width, height);
const uid = texture.uid;
const slab = texture.source;

const item = {
uid: uid,
texture: texture,
asset: asset,
// dirtyId !== updateId only if image loaded
dirtyId: asset instanceof HTMLImageElement && !asset.complete ? -1 : 0,
updateId: -1
avoidSeams: avoidSeams,
uploaded: false
};

slab._items.set(uid, item);

if (asset instanceof HTMLImageElement && !asset.complete) {
asset.addEventListener('load', () => {
if (!texture.destroyed && slab._items.has(uid)) {
item.dirtyId++;
slab.update();
texture.update();
} else {
console.warn('Image loaded after texture was destroyed'); // eslint-disable-line no-console
}
});
}

slab.update();

return texture;
Expand Down Expand Up @@ -214,6 +205,7 @@ const glUploadAtlasResource = {
id: 'atlas',
upload(slab, glTexture, gl, webGLVersion) {
const { width, height } = slab;
const { target, format, type } = glTexture;
const premultipliedAlpha = slab.alphaMode === 'premultiply-alpha-on-upload';

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, premultipliedAlpha);
Expand All @@ -233,27 +225,13 @@ const glUploadAtlasResource = {
// pixels[j+2] = 0;
// pixels[j+3] = 255;
//}

gl.texImage2D(
glTexture.target,
0,
glTexture.format,
width,
height,
0,
glTexture.format,
glTexture.type,
undefined // no copy
// fill red
// gl.RGBA,
// gl.UNSIGNED_BYTE,
// pixels
);
gl.texImage2D(target, 0, format, width, height, 0, format, type, undefined); // no fill
// gl.texImage2D(target, 0, format, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels); // fill red
}

// Upload all atlas items.
for (const item of slab._items.values()) {
if (item.updateId === item.dirtyId) continue;
if (item.uploaded) continue;

const bin = item.texture.__bin;
let source = item.asset;
Expand All @@ -279,19 +257,27 @@ const glUploadAtlasResource = {
}
}

gl.texSubImage2D(
glTexture.target,
0,
bin.x,
bin.y,
bin.width,
bin.height,
glTexture.format,
glTexture.type,
source
);

item.updateId = item.dirtyId;
const { x, y, width: w, height: h } = bin;

// Experiment: bake in 1px padding by duplicating the edge rows/cols - see Rapid#1650
// Because it is too complicated to blit framebuffers, or grab just a few pixels from
// the source image, I'm just going to do this 4x for the corners + 1 for the main image.
// Goal is to avoid having images in the atlas bleed into a neighboring image by the sampler.
if (item.avoidSeams) {
gl.texSubImage2D(target, 0, x-1, y-1, 1, 1, format, type, source); // left top
gl.texSubImage2D(target, 0, x-1, y+1, w, h, format, type, source); // left bottom
gl.texSubImage2D(target, 0, x+1, y-1, w, h, format, type, source); // right top
gl.texSubImage2D(target, 0, x+1, y+1, w, h, format, type, source); // right bottom
gl.texSubImage2D(target, 0, x-1, y, w, h, format, type, source); // left mid
gl.texSubImage2D(target, 0, x+1, y, w, h, format, type, source); // right mid
gl.texSubImage2D(target, 0, x, y-1, w, h, format, type, source); // mid top
gl.texSubImage2D(target, 0, x, y+1, w, h, format, type, source); // mid bottom
}
// end experiment

gl.texSubImage2D(target, 0, x, y, w, h, format, type, source); // the image we really want

item.uploaded = true;
}
}
};
Expand All @@ -304,9 +290,9 @@ const gpuUploadAtlasResource = {
const premultipliedAlpha = slab.alphaMode === 'premultiply-alpha-on-upload';

for (const item of slab._items.values()) {
if (item.updateId === item.dirtyId) continue;
if (item.uploaded) continue;

const bin = item.texture.__bin;
const { x, y, width: w, height: h } = item.texture.__bin;
let source = item.asset;
if (source instanceof ImageData) {
source = source.data;
Expand All @@ -317,49 +303,55 @@ const gpuUploadAtlasResource = {
source instanceof HTMLCanvasElement ||
source instanceof ImageBitmap
) {
gpu.device.queue.copyExternalImageToTexture(
{ // source
source: source
},
{ // destination
origin: {
x: bin.x,
y: bin.y
},
premultipliedAlpha: premultipliedAlpha,
texture: gpuTexture,
},
{ // copySize
height: bin.height,
width: bin.width,
}
);
const src = { source: source };
const origin = { x: x, y: y };
const dest = { origin: origin, premultipliedAlpha: premultipliedAlpha, texture: gpuTexture };
const size = { width: w, height: h };

// Same experiment as the WebGL one above
if (item.avoidSeams) {
origin.x = x-1; origin.y = y-1; // top left
gpu.device.queue.copyExternalImageToTexture(src, dest, size);
origin.x = x-1; origin.y = y+1; // bottom left
gpu.device.queue.copyExternalImageToTexture(src, dest, size);
origin.x = x+1; origin.y = y-1; // top right
gpu.device.queue.copyExternalImageToTexture(src, dest, size);
origin.x = x+1; origin.y = y+1; // bottom right
gpu.device.queue.copyExternalImageToTexture(src, dest, size);
}
// end experiment

origin.x = x; origin.y = y; // the image we really want
gpu.device.queue.copyExternalImageToTexture(src, dest, size);

// writetexture
} else if (ArrayBuffer.isView(source)) {
gpu.device.queue.writeTexture(
{ // destination
origin: {
x: bin.x,
y: bin.y
},
texture: gpuTexture
},
source,
{ // dataLayout
bytesPerRow: source.byteLength / bin.height
},
{ // size
height: bin.height,
width: bin.width
}
);
const origin = { x: x, y: y };
const dest = { origin: origin, texture: gpuTexture };
const layout = { bytesPerRow: source.byteLength / h };
const size = { width: w, height: h };

// Same experiment as the WebGL one above
if (item.avoidSeams) {
origin.x = x-1; origin.y = y-1; // top left
gpu.device.queue.writeTexture(dest, source, layout, size);
origin.x = x-1; origin.y = y+1; // bottom left
gpu.device.queue.writeTexture(dest, source, layout, size);
origin.x = x+1; origin.y = y-1; // top right
gpu.device.queue.writeTexture(dest, source, layout, size);
origin.x = x+1; origin.y = y+1; // bottom right
gpu.device.queue.writeTexture(dest, source, layout, size);
}
// end experiment

origin.x = x; origin.y = y; // the image we really want
gpu.device.queue.writeTexture(dest, source, layout, size);

} else {
throw new Error('Unsupported source type');
}

item.updateId = item.dirtyId;
item.uploaded = true;
}
}
};
Expand Down

0 comments on commit 3bd4723

Please sign in to comment.