1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.graphics; 18 19 /** 20 * @hide 21 */ 22 public class Atlas { 23 /** 24 * WARNING: These flag values are part of the on-disk configuration information, 25 * do not change their values. 26 */ 27 28 /** DELETED: FLAG_ROTATION = 0x01 */ 29 30 /** 31 * This flag indicates whether the packing algorithm should leave 32 * an empty 1 pixel wide border around each bitmap. This border can 33 * be useful if the content of the atlas will be used in OpenGL using 34 * bilinear filtering. 35 */ 36 public static final int FLAG_ADD_PADDING = 0x2; 37 /** 38 * Default flags: allow rotations and add padding. 39 */ 40 public static final int FLAG_DEFAULTS = FLAG_ADD_PADDING; 41 42 /** 43 * Each type defines a different packing algorithm that can 44 * be used by an {@link Atlas}. The best algorithm to use 45 * will depend on the source dataset and the dimensions of 46 * the atlas. 47 */ 48 public enum Type { 49 SliceMinArea, 50 SliceMaxArea, 51 SliceShortAxis, 52 SliceLongAxis 53 } 54 55 /** 56 * Represents a bitmap packed in the atlas. Each entry has a location in 57 * pixels in the atlas and a rotation flag. 58 */ 59 public static class Entry { 60 /** 61 * Location, in pixels, of the bitmap on the X axis in the atlas. 62 */ 63 public int x; 64 /** 65 * Location, in pixels, of the bitmap on the Y axis in the atlas. 66 */ 67 public int y; 68 } 69 70 private final Policy mPolicy; 71 72 /** 73 * Creates a new atlas with the specified algorithm and dimensions 74 * in pixels. Calling this constructor is equivalent to calling 75 * {@link #Atlas(Atlas.Type, int, int, int)} with {@link #FLAG_DEFAULTS}. 76 * 77 * @param type The algorithm to use to pack rectangles in the atlas 78 * @param width The width of the atlas in pixels 79 * @param height The height of the atlas in pixels 80 * 81 * @see #Atlas(Atlas.Type, int, int, int) 82 */ Atlas(Type type, int width, int height)83 public Atlas(Type type, int width, int height) { 84 this(type, width, height, FLAG_DEFAULTS); 85 } 86 87 /** 88 * Creates a new atlas with the specified algorithm and dimensions 89 * in pixels. A set of flags can also be specified to control the 90 * behavior of the atlas. 91 * 92 * @param type The algorithm to use to pack rectangles in the atlas 93 * @param width The width of the atlas in pixels 94 * @param height The height of the atlas in pixels 95 * @param flags Optional flags to control the behavior of the atlas: 96 * {@link #FLAG_ADD_PADDING}, {@link #FLAG_ALLOW_ROTATIONS} 97 * 98 * @see #Atlas(Atlas.Type, int, int) 99 */ Atlas(Type type, int width, int height, int flags)100 public Atlas(Type type, int width, int height, int flags) { 101 mPolicy = findPolicy(type, width, height, flags); 102 } 103 104 /** 105 * Packs a rectangle of the specified dimensions in this atlas. 106 * 107 * @param width The width of the rectangle to pack in the atlas 108 * @param height The height of the rectangle to pack in the atlas 109 * 110 * @return An {@link Entry} instance if the rectangle was packed in 111 * the atlas, or null if the rectangle could not fit 112 * 113 * @see #pack(int, int, Atlas.Entry) 114 */ pack(int width, int height)115 public Entry pack(int width, int height) { 116 return pack(width, height, null); 117 } 118 119 /** 120 * Packs a rectangle of the specified dimensions in this atlas. 121 * 122 * @param width The width of the rectangle to pack in the atlas 123 * @param height The height of the rectangle to pack in the atlas 124 * @param entry Out parameter that will be filled in with the location 125 * and attributes of the packed rectangle, can be null 126 * 127 * @return An {@link Entry} instance if the rectangle was packed in 128 * the atlas, or null if the rectangle could not fit 129 * 130 * @see #pack(int, int) 131 */ pack(int width, int height, Entry entry)132 public Entry pack(int width, int height, Entry entry) { 133 if (entry == null) entry = new Entry(); 134 return mPolicy.pack(width, height, entry); 135 } 136 findPolicy(Type type, int width, int height, int flags)137 private static Policy findPolicy(Type type, int width, int height, int flags) { 138 switch (type) { 139 case SliceMinArea: 140 return new SlicePolicy(width, height, flags, 141 new SlicePolicy.MinAreaSplitDecision()); 142 case SliceMaxArea: 143 return new SlicePolicy(width, height, flags, 144 new SlicePolicy.MaxAreaSplitDecision()); 145 case SliceShortAxis: 146 return new SlicePolicy(width, height, flags, 147 new SlicePolicy.ShorterFreeAxisSplitDecision()); 148 case SliceLongAxis: 149 return new SlicePolicy(width, height, flags, 150 new SlicePolicy.LongerFreeAxisSplitDecision()); 151 } 152 return null; 153 } 154 155 /** 156 * A policy defines how the atlas performs the packing operation. 157 */ 158 private static abstract class Policy { pack(int width, int height, Entry entry)159 abstract Entry pack(int width, int height, Entry entry); 160 } 161 162 /** 163 * The Slice algorightm divides the remaining empty space either 164 * horizontally or vertically after a bitmap is placed in the atlas. 165 * 166 * NOTE: the algorithm is explained below using a tree but is 167 * implemented using a linked list instead for performance reasons. 168 * 169 * The algorithm starts with a single empty cell covering the entire 170 * atlas: 171 * 172 * ----------------------- 173 * | | 174 * | | 175 * | | 176 * | Empty space | 177 * | (C0) | 178 * | | 179 * | | 180 * | | 181 * ----------------------- 182 * 183 * The tree of cells looks like this: 184 * 185 * N0(free) 186 * 187 * The algorithm then places a bitmap B1, if possible: 188 * 189 * ----------------------- 190 * | | | 191 * | B1 | | 192 * | | | 193 * |-------- | 194 * | | 195 * | | 196 * | | 197 * | | 198 * ----------------------- 199 * 200 * After placing a bitmap in an empty cell, the algorithm splits 201 * the remaining space in two new empty cells. The split can occur 202 * vertically or horizontally (this is controlled by the "split 203 * decision" parameter of the algorithm.) 204 * 205 * Here is for the instance the result of a vertical split: 206 * 207 * ----------------------- 208 * | | | 209 * | B1 | | 210 * | | | 211 * |--------| C2 | 212 * | | | 213 * | | | 214 * | C1 | | 215 * | | | 216 * ----------------------- 217 * 218 * The cells tree now looks like this: 219 * 220 * C0(occupied) 221 * / \ 222 * / \ 223 * / \ 224 * / \ 225 * C1(free) C2(free) 226 * 227 * For each bitmap to place in the atlas, the Slice algorithm 228 * will visit the free cells until it finds one where a bitmap can 229 * fit. It will then split the now occupied cell and proceed onto 230 * the next bitmap. 231 */ 232 private static class SlicePolicy extends Policy { 233 private final Cell mRoot = new Cell(); 234 235 private final SplitDecision mSplitDecision; 236 237 private final int mPadding; 238 239 /** 240 * A cell represents a sub-rectangle of the atlas. A cell is 241 * a node in a linked list representing the available free 242 * space in the atlas. 243 */ 244 private static class Cell { 245 int x; 246 int y; 247 248 int width; 249 int height; 250 251 Cell next; 252 253 @Override toString()254 public String toString() { 255 return String.format("cell[x=%d y=%d width=%d height=%d", x, y, width, height); 256 } 257 } 258 SlicePolicy(int width, int height, int flags, SplitDecision splitDecision)259 SlicePolicy(int width, int height, int flags, SplitDecision splitDecision) { 260 mPadding = (flags & FLAG_ADD_PADDING) != 0 ? 1 : 0; 261 262 // The entire atlas is empty at first, minus padding 263 Cell first = new Cell(); 264 first.x = first.y = mPadding; 265 first.width = width - 2 * mPadding; 266 first.height = height - 2 * mPadding; 267 268 mRoot.next = first; 269 mSplitDecision = splitDecision; 270 } 271 272 @Override pack(int width, int height, Entry entry)273 Entry pack(int width, int height, Entry entry) { 274 Cell cell = mRoot.next; 275 Cell prev = mRoot; 276 277 while (cell != null) { 278 if (insert(cell, prev, width, height, entry)) { 279 return entry; 280 } 281 282 prev = cell; 283 cell = cell.next; 284 } 285 286 return null; 287 } 288 289 /** 290 * Defines how the remaining empty space should be split up: 291 * vertically or horizontally. 292 */ 293 private static interface SplitDecision { 294 /** 295 * Returns true if the remaining space defined by 296 * <code>freeWidth</code> and <code>freeHeight</code> 297 * should be split horizontally. 298 * 299 * @param freeWidth The rectWidth of the free space after packing a rectangle 300 * @param freeHeight The rectHeight of the free space after packing a rectangle 301 * @param rectWidth The rectWidth of the rectangle that was packed in a cell 302 * @param rectHeight The rectHeight of the rectangle that was packed in a cell 303 */ splitHorizontal(int freeWidth, int freeHeight, int rectWidth, int rectHeight)304 boolean splitHorizontal(int freeWidth, int freeHeight, 305 int rectWidth, int rectHeight); 306 } 307 308 // Splits the free area horizontally to minimize the horizontal section area 309 private static class MinAreaSplitDecision implements SplitDecision { 310 @Override splitHorizontal(int freeWidth, int freeHeight, int rectWidth, int rectHeight)311 public boolean splitHorizontal(int freeWidth, int freeHeight, 312 int rectWidth, int rectHeight) { 313 return rectWidth * freeHeight > freeWidth * rectHeight; 314 } 315 } 316 317 // Splits the free area horizontally to maximize the horizontal section area 318 private static class MaxAreaSplitDecision implements SplitDecision { 319 @Override splitHorizontal(int freeWidth, int freeHeight, int rectWidth, int rectHeight)320 public boolean splitHorizontal(int freeWidth, int freeHeight, 321 int rectWidth, int rectHeight) { 322 return rectWidth * freeHeight <= freeWidth * rectHeight; 323 } 324 } 325 326 // Splits the free area horizontally if the horizontal axis is shorter 327 private static class ShorterFreeAxisSplitDecision implements SplitDecision { 328 @Override splitHorizontal(int freeWidth, int freeHeight, int rectWidth, int rectHeight)329 public boolean splitHorizontal(int freeWidth, int freeHeight, 330 int rectWidth, int rectHeight) { 331 return freeWidth <= freeHeight; 332 } 333 } 334 335 // Splits the free area horizontally if the vertical axis is shorter 336 private static class LongerFreeAxisSplitDecision implements SplitDecision { 337 @Override splitHorizontal(int freeWidth, int freeHeight, int rectWidth, int rectHeight)338 public boolean splitHorizontal(int freeWidth, int freeHeight, 339 int rectWidth, int rectHeight) { 340 return freeWidth > freeHeight; 341 } 342 } 343 344 /** 345 * Attempts to pack a rectangle of specified dimensions in the available 346 * empty space. 347 * 348 * @param cell The cell representing free space in which to pack the rectangle 349 * @param prev The previous cell in the free space linked list 350 * @param width The width of the rectangle to pack 351 * @param height The height of the rectangle to pack 352 * @param entry Stores the location of the packged rectangle, if it fits 353 * 354 * @return True if the rectangle was packed in the atlas, false otherwise 355 */ insert(Cell cell, Cell prev, int width, int height, Entry entry)356 private boolean insert(Cell cell, Cell prev, int width, int height, Entry entry) { 357 if (cell.width < width || cell.height < height) { 358 return false; 359 } 360 361 // Remaining free space after packing the rectangle 362 int deltaWidth = cell.width - width; 363 int deltaHeight = cell.height - height; 364 365 // Split the remaining free space into two new cells 366 Cell first = new Cell(); 367 Cell second = new Cell(); 368 369 first.x = cell.x + width + mPadding; 370 first.y = cell.y; 371 first.width = deltaWidth - mPadding; 372 373 second.x = cell.x; 374 second.y = cell.y + height + mPadding; 375 second.height = deltaHeight - mPadding; 376 377 if (mSplitDecision.splitHorizontal(deltaWidth, deltaHeight, 378 width, height)) { 379 first.height = height; 380 second.width = cell.width; 381 } else { 382 first.height = cell.height; 383 second.width = width; 384 385 // The order of the cells matters for efficient packing 386 // We want to give priority to the cell chosen by the 387 // split decision heuristic 388 Cell temp = first; 389 first = second; 390 second = temp; 391 } 392 393 // Remove degenerate cases to keep the free list as small as possible 394 if (first.width > 0 && first.height > 0) { 395 prev.next = first; 396 prev = first; 397 } 398 399 if (second.width > 0 && second.height > 0) { 400 prev.next = second; 401 second.next = cell.next; 402 } else { 403 prev.next = cell.next; 404 } 405 406 // The cell is now completely removed from the free list 407 cell.next = null; 408 409 // Return the location and rotation of the packed rectangle 410 entry.x = cell.x; 411 entry.y = cell.y; 412 413 return true; 414 } 415 } 416 } 417