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