From 8dd624d4e09f68d4a8739f1d140a983cae7e23e0 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Wed, 29 Aug 2018 22:10:41 +0300 Subject: [PATCH] Improve image/glyph atlas packing algorithm (#7171) * use a better packing algorithm for image/glyph atlas * make sure the atlas is non-zero to fix render tests * switch to the external potpack package * replace shelf-pack with potpack in ImageManager --- flow-typed/potpack.js | 12 +++++ flow-typed/shelf-pack.js | 25 ----------- package.json | 2 +- src/render/glyph_atlas.js | 9 ++-- src/render/image_atlas.js | 8 ++-- src/render/image_manager.js | 87 ++++++++++++++++++------------------- yarn.lock | 8 ++-- 7 files changed, 66 insertions(+), 85 deletions(-) create mode 100644 flow-typed/potpack.js delete mode 100644 flow-typed/shelf-pack.js diff --git a/flow-typed/potpack.js b/flow-typed/potpack.js new file mode 100644 index 00000000000..48fe21c825e --- /dev/null +++ b/flow-typed/potpack.js @@ -0,0 +1,12 @@ +declare module "potpack" { + declare type Bin = { + x: number, + y: number, + w: number, + h: number + }; + + declare function potpack(bins: Array): {w: number, h: number, fill: number}; + + declare module.exports: typeof potpack; +} diff --git a/flow-typed/shelf-pack.js b/flow-typed/shelf-pack.js deleted file mode 100644 index 49d092b26f3..00000000000 --- a/flow-typed/shelf-pack.js +++ /dev/null @@ -1,25 +0,0 @@ -declare module "@mapbox/shelf-pack" { - declare type Bin = { - id: number|string, - x: number, - y: number, - w: number, - h: number - }; - - declare class ShelfPack { - w: number; - h: number; - - constructor(w: number, h: number, options?: {autoResize: boolean}): ShelfPack; - - pack(bins: Array<{w: number, h: number}>, options?: {inPlace: boolean}): Array; - packOne(w: number, h: number, id?: number|string): Bin; - shrink(): void; - - ref(bin: Bin): number; - unref(bin: Bin): number; - } - - declare module.exports: typeof ShelfPack; -} diff --git a/package.json b/package.json index 657f9266ee6..306d9fecc13 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/mapbox-gl-supported": "^1.4.0", "@mapbox/point-geometry": "^0.1.0", - "@mapbox/shelf-pack": "^3.2.0", "@mapbox/tiny-sdf": "^1.1.0", "@mapbox/unitbezier": "^0.0.0", "@mapbox/vector-tile": "^1.3.1", @@ -75,6 +74,7 @@ "pngjs": "^3.0.0", "postcss-cli": "^5.0.0", "postcss-inline-svg": "^3.1.1", + "potpack": "^1.0.1", "pretty-bytes": "^5.1.0", "prismjs": "^1.8.1", "prop-types": "^15.6.0", diff --git a/src/render/glyph_atlas.js b/src/render/glyph_atlas.js index 06df12acf04..885fe46230a 100644 --- a/src/render/glyph_atlas.js +++ b/src/render/glyph_atlas.js @@ -1,9 +1,8 @@ // @flow -import ShelfPack from '@mapbox/shelf-pack'; - import { AlphaImage } from '../util/image'; import { register } from '../util/web_worker_transfer'; +import potpack from 'potpack'; import type {GlyphMetrics, StyleGlyph} from '../style/style_glyph'; @@ -27,7 +26,6 @@ export default class GlyphAtlas { constructor(stacks: { [string]: { [number]: ?StyleGlyph } }) { const positions = {}; - const pack = new ShelfPack(0, 0, {autoResize: true}); const bins = []; for (const stack in stacks) { @@ -49,9 +47,8 @@ export default class GlyphAtlas { } } - pack.pack(bins, {inPlace: true}); - - const image = new AlphaImage({width: pack.w, height: pack.h}); + const {w, h} = potpack(bins); + const image = new AlphaImage({width: w, height: h}); for (const stack in stacks) { const glyphs = stacks[stack]; diff --git a/src/render/image_atlas.js b/src/render/image_atlas.js index 79a5018ff6b..2af705e76ba 100644 --- a/src/render/image_atlas.js +++ b/src/render/image_atlas.js @@ -1,9 +1,8 @@ // @flow -import ShelfPack from '@mapbox/shelf-pack'; - import { RGBAImage } from '../util/image'; import { register } from '../util/web_worker_transfer'; +import potpack from 'potpack'; import type {StyleImage} from '../style/style_image'; @@ -61,7 +60,6 @@ export default class ImageAtlas { constructor(icons: {[string]: StyleImage}, patterns: {[string]: StyleImage}) { const iconPositions = {}, patternPositions = {}; - const pack = new ShelfPack(0, 0, {autoResize: true}); const bins = []; for (const id in icons) { const src = icons[id]; @@ -87,9 +85,9 @@ export default class ImageAtlas { patternPositions[id] = new ImagePosition(bin, src); } - pack.pack(bins, {inPlace: true}); + const {w, h} = potpack(bins); + const image = new RGBAImage({width: w || 1, height: h || 1}); - const image = new RGBAImage({width: pack.w, height: pack.h}); for (const id in icons) { const src = icons[id]; const bin = iconPositions[id].paddedRect; diff --git a/src/render/image_manager.js b/src/render/image_manager.js index e45145a3fab..3b8677174f8 100644 --- a/src/render/image_manager.js +++ b/src/render/image_manager.js @@ -1,6 +1,6 @@ // @flow -import ShelfPack from '@mapbox/shelf-pack'; +import potpack from 'potpack'; import { RGBAImage } from '../util/image'; import { ImagePosition } from './image_atlas'; @@ -9,7 +9,7 @@ import assert from 'assert'; import type {StyleImage} from '../style/style_image'; import type Context from '../gl/context'; -import type {Bin} from '@mapbox/shelf-pack'; +import type {Bin} from 'potpack'; import type {Callback} from '../types/callback'; type Pattern = { @@ -38,7 +38,6 @@ class ImageManager { loaded: boolean; requestors: Array<{ids: Array, callback: Callback<{[string]: StyleImage}>}>; - shelfPack: ShelfPack; patterns: {[string]: Pattern}; atlasImage: RGBAImage; atlasTexture: ?Texture; @@ -49,9 +48,8 @@ class ImageManager { this.loaded = false; this.requestors = []; - this.shelfPack = new ShelfPack(64, 64, {autoResize: true}); this.patterns = {}; - this.atlasImage = new RGBAImage({width: 64, height: 64}); + this.atlasImage = new RGBAImage({width: 1, height: 1}); this.dirty = true; } @@ -86,12 +84,7 @@ class ImageManager { removeImage(id: string) { assert(this.images[id]); delete this.images[id]; - - const pattern = this.patterns[id]; - if (pattern) { - this.shelfPack.unref(pattern.bin); - delete this.patterns[id]; - } + delete this.patterns[id]; } listImages(): Array { @@ -139,10 +132,8 @@ class ImageManager { // Pattern stuff getPixelSize() { - return { - width: this.shelfPack.w, - height: this.shelfPack.h - }; + const {width, height} = this.atlasImage; + return {width, height}; } getPattern(id: string): ?ImagePosition { @@ -156,36 +147,13 @@ class ImageManager { return null; } - const width = image.data.width + padding * 2; - const height = image.data.height + padding * 2; - - const bin = this.shelfPack.packOne(width, height); - if (!bin) { - return null; - } - - this.atlasImage.resize(this.getPixelSize()); - - const src = image.data; - const dst = this.atlasImage; - - const x = bin.x + padding; - const y = bin.y + padding; - const w = src.width; - const h = src.height; - - RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x, y }, { width: w, height: h }); - - // Add 1 pixel wrapped padding on each side of the image. - RGBAImage.copy(src, dst, { x: 0, y: h - 1 }, { x: x, y: y - 1 }, { width: w, height: 1 }); // T - RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x, y: y + h }, { width: w, height: 1 }); // B - RGBAImage.copy(src, dst, { x: w - 1, y: 0 }, { x: x - 1, y: y }, { width: 1, height: h }); // L - RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x + w, y: y }, { width: 1, height: h }); // R - - this.dirty = true; - + const w = image.data.width + padding * 2; + const h = image.data.height + padding * 2; + const bin = {w, h, x: 0, y: 0}; const position = new ImagePosition(bin, image); - this.patterns[id] = { bin, position }; + this.patterns[id] = {bin, position}; + this._updatePatternAtlas(); + return position; } @@ -200,6 +168,37 @@ class ImageManager { this.atlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); } + + _updatePatternAtlas() { + const bins = []; + for (const id in this.patterns) { + bins.push(this.patterns[id].bin); + } + + const {w, h} = potpack(bins); + + const dst = this.atlasImage; + dst.resize({width: w, height: h}); + + for (const id in this.patterns) { + const {bin} = this.patterns[id]; + const x = bin.x + padding; + const y = bin.y + padding; + const src = this.images[id].data; + const w = src.width; + const h = src.height; + + RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x, y }, { width: w, height: h }); + + // Add 1 pixel wrapped padding on each side of the image. + RGBAImage.copy(src, dst, { x: 0, y: h - 1 }, { x: x, y: y - 1 }, { width: w, height: 1 }); // T + RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x, y: y + h }, { width: w, height: 1 }); // B + RGBAImage.copy(src, dst, { x: w - 1, y: 0 }, { x: x - 1, y: y }, { width: 1, height: h }); // L + RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x + w, y: y }, { width: 1, height: h }); // R + } + + this.dirty = true; + } } export default ImageManager; diff --git a/yarn.lock b/yarn.lock index c2d4d1e980a..c84d52f76d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -239,10 +239,6 @@ debounce "^1.0.2" xtend "^4.0.1" -"@mapbox/shelf-pack@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@mapbox/shelf-pack/-/shelf-pack-3.2.0.tgz#df3630ecce8c042817c9a365b88078412963de64" - "@mapbox/sphericalmercator@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@mapbox/sphericalmercator/-/sphericalmercator-1.0.5.tgz#70237b9774095ed1cfdbcea7a8fd1fc82b2691f2" @@ -7715,6 +7711,10 @@ postcss@^6.0.11, postcss@^6.0.21, postcss@^6.0.23: source-map "^0.6.1" supports-color "^5.4.0" +potpack@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.1.tgz#d1b1afd89e4c8f7762865ec30bd112ab767e2ebf" + prebuild-install@^2.1.1: version "2.5.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.5.1.tgz#0f234140a73760813657c413cdccdda58296b1da"