babylon.rectPackingMap.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. module BABYLON {
  2. /**
  3. * The purpose of this class is to pack several Rectangles into a big map, while trying to fit everything as optimaly as possible.
  4. * This class is typically used to build lightmaps, sprite map or to pack several little textures into a big one.
  5. * Note that this class allows allocated Rectangles to be freed: that is the map is dynamically maintained so you can add/remove rectangle based on their lifecycle.
  6. */
  7. export class RectPackingMap extends PackedRect {
  8. /**
  9. * Create an instance of the object with a dimension using the given size
  10. * @param size The dimension of the rectangle that will contain all the sub ones.
  11. */
  12. constructor(size: Size) {
  13. super(null, null, Vector2.Zero(), size);
  14. this._root = this;
  15. }
  16. /**
  17. * Add a rectangle, finding the best location to store it into the map
  18. * @param size the dimension of the rectangle to store
  19. * @return the Node containing the rectangle information, or null if we couldn't find a free spot
  20. */
  21. public addRect(size: Size): PackedRect {
  22. var node = this.findAndSplitNode(size);
  23. return node;
  24. }
  25. /**
  26. * Return the current space free normalized between [0;1]
  27. * @returns {}
  28. */
  29. public get freeSpace(): number {
  30. var freeSize = 0;
  31. freeSize = this.evalFreeSize(freeSize);
  32. return freeSize / (this._size.width * this._size.height);
  33. }
  34. }
  35. /**
  36. * This class describe a rectangle that were added to the map.
  37. * You have access to its coordinates either in pixel or normalized (UV)
  38. */
  39. export class PackedRect {
  40. constructor(root: PackedRect, parent: PackedRect, pos: Vector2, size: Size) {
  41. this._pos = pos;
  42. this._size = size;
  43. this._root = root;
  44. this._parent = parent;
  45. }
  46. /**
  47. * @returns the position of this node into the map
  48. */
  49. public get pos() {
  50. return this._pos;
  51. }
  52. /**
  53. * @returns the size of the rectangle this node handles
  54. */
  55. public get contentSize(): Size {
  56. return this._contentSize;
  57. }
  58. /**
  59. * Compute the UV of the top/left, top/right, bottom/right, bottom/left points of the rectangle this node handles into the map
  60. * @returns And array of 4 Vector2, containing UV coordinates for the four corners of the Rectangle into the map
  61. */
  62. public get UVs(): Vector2[] {
  63. var mainWidth = this._root._size.width;
  64. var mainHeight = this._root._size.height;
  65. var topLeft = new Vector2(this._pos.x / mainWidth, this._pos.y / mainHeight);
  66. var rightBottom = new Vector2((this._pos.x + this._contentSize.width - 1) / mainWidth, (this._pos.y + this._contentSize.height - 1) / mainHeight);
  67. var uvs = new Array<Vector2>();
  68. uvs.push(topLeft);
  69. uvs.push(new Vector2(rightBottom.x, topLeft.y));
  70. uvs.push(rightBottom);
  71. uvs.push(new Vector2(topLeft.x, rightBottom.y));
  72. return uvs;
  73. }
  74. /**
  75. * Free this rectangle from the map.
  76. * Call this method when you no longer need the rectangle to be in the map.
  77. */
  78. public freeContent() {
  79. if (!this.contentSize) {
  80. return;
  81. }
  82. this._contentSize = null;
  83. // If everything below is also free, reset the whole node, and attempt to reset parents if they also become free
  84. this.attemptDefrag();
  85. }
  86. protected get isUsed(): boolean {
  87. return this._contentSize != null || this._leftNode != null;
  88. }
  89. protected findAndSplitNode(contentSize: Size): PackedRect {
  90. var node = this.findNode(contentSize);
  91. // Not enought space...
  92. if (!node) {
  93. return null;
  94. }
  95. node.splitNode(contentSize);
  96. return node;
  97. }
  98. private findNode(size: Size): PackedRect {
  99. var resNode: PackedRect = null;
  100. // If this node is used, recurse to each of his subNodes to find an available one in its branch
  101. if (this.isUsed) {
  102. if (this._leftNode) {
  103. resNode = this._leftNode.findNode(size);
  104. }
  105. if (!resNode && this._rightNode) {
  106. resNode = this._rightNode.findNode(size);
  107. }
  108. if (!resNode && this._bottomNode) {
  109. resNode = this._bottomNode.findNode(size);
  110. }
  111. }
  112. // The node is free, but was previously allocated (_initialSize is set), rely on initialSize to make the test as it's the space we have
  113. else if (this._initialSize && (size.width <= this._initialSize.width) && (size.height <= this._initialSize.height)) {
  114. resNode = this;
  115. }
  116. // The node is free and empty, rely on its size for the test
  117. else if ((size.width <= this._size.width) && (size.height <= this._size.height)) {
  118. resNode = this;
  119. }
  120. return resNode;
  121. }
  122. private splitNode(contentSize: Size): PackedRect {
  123. // If there's no contentSize but an initialSize it means this node were previously allocated, but freed, we need to create a _leftNode as subNode and use to allocate the space we need (and this node will have a right/bottom subNode for the space left as this._initialSize may be greater than contentSize)
  124. if (!this._contentSize && this._initialSize) {
  125. this._leftNode = new PackedRect(this._root, this, new Vector2(this._pos.x, this._pos.y), new Size(this._initialSize.width, this._initialSize.height));
  126. return this._leftNode.splitNode(contentSize);
  127. } else {
  128. this._contentSize = contentSize.clone();
  129. this._initialSize = contentSize.clone();
  130. if (contentSize.width !== this._size.width) {
  131. this._rightNode = new PackedRect(this._root, this, new Vector2(this._pos.x + contentSize.width, this._pos.y), new Size(this._size.width - contentSize.width, contentSize.height));
  132. }
  133. if (contentSize.height !== this._size.height) {
  134. this._bottomNode = new PackedRect(this._root, this, new Vector2(this._pos.x, this._pos.y + contentSize.height), new Size(this._size.width, this._size.height - contentSize.height));
  135. }
  136. return this;
  137. }
  138. }
  139. private attemptDefrag() {
  140. if (!this.isUsed && this.isRecursiveFree) {
  141. this.clearNode();
  142. if (this._parent) {
  143. this._parent.attemptDefrag();
  144. }
  145. }
  146. }
  147. private clearNode() {
  148. this._initialSize = null;
  149. this._rightNode = null;
  150. this._bottomNode = null;
  151. }
  152. private get isRecursiveFree() {
  153. return !this.contentSize && (!this._leftNode || this._leftNode.isRecursiveFree) && (!this._rightNode || this._rightNode.isRecursiveFree) && (!this._bottomNode || this._bottomNode.isRecursiveFree);
  154. }
  155. protected evalFreeSize(size: number): number {
  156. var levelSize = 0;
  157. if (!this.isUsed) {
  158. if (this._initialSize) {
  159. levelSize = this._initialSize.surface;
  160. } else {
  161. levelSize = this._size.surface;
  162. }
  163. }
  164. if (this._rightNode) {
  165. levelSize += this._rightNode.evalFreeSize(0);
  166. }
  167. if (this._bottomNode) {
  168. levelSize += this._bottomNode.evalFreeSize(0);
  169. }
  170. return levelSize + size;
  171. }
  172. protected _root: PackedRect;
  173. protected _parent: PackedRect;
  174. private _contentSize: Size;
  175. private _initialSize: Size;
  176. private _leftNode: PackedRect;
  177. private _rightNode: PackedRect;
  178. private _bottomNode: PackedRect;
  179. private _pos: Vector2;
  180. protected _size: Size;
  181. }
  182. }