TextureAtlas.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. import BoundingRectangle from '../Core/BoundingRectangle.js';
  2. import Cartesian2 from '../Core/Cartesian2.js';
  3. import createGuid from '../Core/createGuid.js';
  4. import defaultValue from '../Core/defaultValue.js';
  5. import defined from '../Core/defined.js';
  6. import defineProperties from '../Core/defineProperties.js';
  7. import destroyObject from '../Core/destroyObject.js';
  8. import DeveloperError from '../Core/DeveloperError.js';
  9. import PixelFormat from '../Core/PixelFormat.js';
  10. import Resource from '../Core/Resource.js';
  11. import RuntimeError from '../Core/RuntimeError.js';
  12. import Framebuffer from '../Renderer/Framebuffer.js';
  13. import Texture from '../Renderer/Texture.js';
  14. import when from '../ThirdParty/when.js';
  15. // The atlas is made up of regions of space called nodes that contain images or child nodes.
  16. function TextureAtlasNode(bottomLeft, topRight, childNode1, childNode2, imageIndex) {
  17. this.bottomLeft = defaultValue(bottomLeft, Cartesian2.ZERO);
  18. this.topRight = defaultValue(topRight, Cartesian2.ZERO);
  19. this.childNode1 = childNode1;
  20. this.childNode2 = childNode2;
  21. this.imageIndex = imageIndex;
  22. }
  23. var defaultInitialSize = new Cartesian2(16.0, 16.0);
  24. /**
  25. * A TextureAtlas stores multiple images in one square texture and keeps
  26. * track of the texture coordinates for each image. TextureAtlas is dynamic,
  27. * meaning new images can be added at any point in time.
  28. * Texture coordinates are subject to change if the texture atlas resizes, so it is
  29. * important to check {@link TextureAtlas#getGUID} before using old values.
  30. *
  31. * @alias TextureAtlas
  32. * @constructor
  33. *
  34. * @param {Object} options Object with the following properties:
  35. * @param {Scene} options.context The context in which the texture gets created.
  36. * @param {PixelFormat} [options.pixelFormat=PixelFormat.RGBA] The pixel format of the texture.
  37. * @param {Number} [options.borderWidthInPixels=1] The amount of spacing between adjacent images in pixels.
  38. * @param {Cartesian2} [options.initialSize=new Cartesian2(16.0, 16.0)] The initial side lengths of the texture.
  39. *
  40. * @exception {DeveloperError} borderWidthInPixels must be greater than or equal to zero.
  41. * @exception {DeveloperError} initialSize must be greater than zero.
  42. *
  43. * @private
  44. */
  45. function TextureAtlas(options) {
  46. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  47. var borderWidthInPixels = defaultValue(options.borderWidthInPixels, 1.0);
  48. var initialSize = defaultValue(options.initialSize, defaultInitialSize);
  49. //>>includeStart('debug', pragmas.debug);
  50. if (!defined(options.context)) {
  51. throw new DeveloperError('context is required.');
  52. }
  53. if (borderWidthInPixels < 0) {
  54. throw new DeveloperError('borderWidthInPixels must be greater than or equal to zero.');
  55. }
  56. if (initialSize.x < 1 || initialSize.y < 1) {
  57. throw new DeveloperError('initialSize must be greater than zero.');
  58. }
  59. //>>includeEnd('debug');
  60. this._context = options.context;
  61. this._pixelFormat = defaultValue(options.pixelFormat, PixelFormat.RGBA);
  62. this._borderWidthInPixels = borderWidthInPixels;
  63. this._textureCoordinates = [];
  64. this._guid = createGuid();
  65. this._idHash = {};
  66. this._initialSize = initialSize;
  67. this._root = undefined;
  68. }
  69. defineProperties(TextureAtlas.prototype, {
  70. /**
  71. * The amount of spacing between adjacent images in pixels.
  72. * @memberof TextureAtlas.prototype
  73. * @type {Number}
  74. */
  75. borderWidthInPixels : {
  76. get : function() {
  77. return this._borderWidthInPixels;
  78. }
  79. },
  80. /**
  81. * An array of {@link BoundingRectangle} texture coordinate regions for all the images in the texture atlas.
  82. * The x and y values of the rectangle correspond to the bottom-left corner of the texture coordinate.
  83. * The coordinates are in the order that the corresponding images were added to the atlas.
  84. * @memberof TextureAtlas.prototype
  85. * @type {BoundingRectangle[]}
  86. */
  87. textureCoordinates : {
  88. get : function() {
  89. return this._textureCoordinates;
  90. }
  91. },
  92. /**
  93. * The texture that all of the images are being written to.
  94. * @memberof TextureAtlas.prototype
  95. * @type {Texture}
  96. */
  97. texture : {
  98. get : function() {
  99. if(!defined(this._texture)) {
  100. this._texture = new Texture({
  101. context : this._context,
  102. width : this._initialSize.x,
  103. height : this._initialSize.y,
  104. pixelFormat : this._pixelFormat
  105. });
  106. }
  107. return this._texture;
  108. }
  109. },
  110. /**
  111. * The number of images in the texture atlas. This value increases
  112. * every time addImage or addImages is called.
  113. * Texture coordinates are subject to change if the texture atlas resizes, so it is
  114. * important to check {@link TextureAtlas#getGUID} before using old values.
  115. * @memberof TextureAtlas.prototype
  116. * @type {Number}
  117. */
  118. numberOfImages : {
  119. get : function() {
  120. return this._textureCoordinates.length;
  121. }
  122. },
  123. /**
  124. * The atlas' globally unique identifier (GUID).
  125. * The GUID changes whenever the texture atlas is modified.
  126. * Classes that use a texture atlas should check if the GUID
  127. * has changed before processing the atlas data.
  128. * @memberof TextureAtlas.prototype
  129. * @type {String}
  130. */
  131. guid : {
  132. get : function() {
  133. return this._guid;
  134. }
  135. }
  136. });
  137. // Builds a larger texture and copies the old texture into the new one.
  138. function resizeAtlas(textureAtlas, image) {
  139. var context = textureAtlas._context;
  140. var numImages = textureAtlas.numberOfImages;
  141. var scalingFactor = 2.0;
  142. var borderWidthInPixels = textureAtlas._borderWidthInPixels;
  143. if (numImages > 0) {
  144. var oldAtlasWidth = textureAtlas._texture.width;
  145. var oldAtlasHeight = textureAtlas._texture.height;
  146. var atlasWidth = scalingFactor * (oldAtlasWidth + image.width + borderWidthInPixels);
  147. var atlasHeight = scalingFactor * (oldAtlasHeight + image.height + borderWidthInPixels);
  148. var widthRatio = oldAtlasWidth / atlasWidth;
  149. var heightRatio = oldAtlasHeight / atlasHeight;
  150. // Create new node structure, putting the old root node in the bottom left.
  151. var nodeBottomRight = new TextureAtlasNode(new Cartesian2(oldAtlasWidth + borderWidthInPixels, borderWidthInPixels), new Cartesian2(atlasWidth, oldAtlasHeight));
  152. var nodeBottomHalf = new TextureAtlasNode(new Cartesian2(), new Cartesian2(atlasWidth, oldAtlasHeight), textureAtlas._root, nodeBottomRight);
  153. var nodeTopHalf = new TextureAtlasNode(new Cartesian2(borderWidthInPixels, oldAtlasHeight + borderWidthInPixels), new Cartesian2(atlasWidth, atlasHeight));
  154. var nodeMain = new TextureAtlasNode(new Cartesian2(), new Cartesian2(atlasWidth, atlasHeight), nodeBottomHalf, nodeTopHalf);
  155. // Resize texture coordinates.
  156. for (var i = 0; i < textureAtlas._textureCoordinates.length; i++) {
  157. var texCoord = textureAtlas._textureCoordinates[i];
  158. if (defined(texCoord)) {
  159. texCoord.x *= widthRatio;
  160. texCoord.y *= heightRatio;
  161. texCoord.width *= widthRatio;
  162. texCoord.height *= heightRatio;
  163. }
  164. }
  165. // Copy larger texture.
  166. var newTexture = new Texture({
  167. context : textureAtlas._context,
  168. width : atlasWidth,
  169. height : atlasHeight,
  170. pixelFormat : textureAtlas._pixelFormat
  171. });
  172. var framebuffer = new Framebuffer({
  173. context : context,
  174. colorTextures : [textureAtlas._texture],
  175. destroyAttachments : false
  176. });
  177. framebuffer._bind();
  178. newTexture.copyFromFramebuffer(0, 0, 0, 0, atlasWidth, atlasHeight);
  179. framebuffer._unBind();
  180. framebuffer.destroy();
  181. textureAtlas._texture = textureAtlas._texture && textureAtlas._texture.destroy();
  182. textureAtlas._texture = newTexture;
  183. textureAtlas._root = nodeMain;
  184. } else {
  185. // First image exceeds initialSize
  186. var initialWidth = scalingFactor * (image.width + 2 * borderWidthInPixels);
  187. var initialHeight = scalingFactor * (image.height + 2 * borderWidthInPixels);
  188. if(initialWidth < textureAtlas._initialSize.x) {
  189. initialWidth = textureAtlas._initialSize.x;
  190. }
  191. if(initialHeight < textureAtlas._initialSize.y) {
  192. initialHeight = textureAtlas._initialSize.y;
  193. }
  194. textureAtlas._texture = textureAtlas._texture && textureAtlas._texture.destroy();
  195. textureAtlas._texture = new Texture({
  196. context : textureAtlas._context,
  197. width : initialWidth,
  198. height : initialHeight,
  199. pixelFormat : textureAtlas._pixelFormat
  200. });
  201. textureAtlas._root = new TextureAtlasNode(new Cartesian2(borderWidthInPixels, borderWidthInPixels),
  202. new Cartesian2(initialWidth, initialHeight));
  203. }
  204. }
  205. // A recursive function that finds the best place to insert
  206. // a new image based on existing image 'nodes'.
  207. // Inspired by: http://blackpawn.com/texts/lightmaps/default.html
  208. function findNode(textureAtlas, node, image) {
  209. if (!defined(node)) {
  210. return undefined;
  211. }
  212. // If a leaf node
  213. if (!defined(node.childNode1) &&
  214. !defined(node.childNode2)) {
  215. // Node already contains an image, don't add to it.
  216. if (defined(node.imageIndex)) {
  217. return undefined;
  218. }
  219. var nodeWidth = node.topRight.x - node.bottomLeft.x;
  220. var nodeHeight = node.topRight.y - node.bottomLeft.y;
  221. var widthDifference = nodeWidth - image.width;
  222. var heightDifference = nodeHeight - image.height;
  223. // Node is smaller than the image.
  224. if (widthDifference < 0 || heightDifference < 0) {
  225. return undefined;
  226. }
  227. // If the node is the same size as the image, return the node
  228. if (widthDifference === 0 && heightDifference === 0) {
  229. return node;
  230. }
  231. // Vertical split (childNode1 = left half, childNode2 = right half).
  232. if (widthDifference > heightDifference) {
  233. node.childNode1 = new TextureAtlasNode(new Cartesian2(node.bottomLeft.x, node.bottomLeft.y), new Cartesian2(node.bottomLeft.x + image.width, node.topRight.y));
  234. // Only make a second child if the border gives enough space.
  235. var childNode2BottomLeftX = node.bottomLeft.x + image.width + textureAtlas._borderWidthInPixels;
  236. if (childNode2BottomLeftX < node.topRight.x) {
  237. node.childNode2 = new TextureAtlasNode(new Cartesian2(childNode2BottomLeftX, node.bottomLeft.y), new Cartesian2(node.topRight.x, node.topRight.y));
  238. }
  239. }
  240. // Horizontal split (childNode1 = bottom half, childNode2 = top half).
  241. else {
  242. node.childNode1 = new TextureAtlasNode(new Cartesian2(node.bottomLeft.x, node.bottomLeft.y), new Cartesian2(node.topRight.x, node.bottomLeft.y + image.height));
  243. // Only make a second child if the border gives enough space.
  244. var childNode2BottomLeftY = node.bottomLeft.y + image.height + textureAtlas._borderWidthInPixels;
  245. if (childNode2BottomLeftY < node.topRight.y) {
  246. node.childNode2 = new TextureAtlasNode(new Cartesian2(node.bottomLeft.x, childNode2BottomLeftY), new Cartesian2(node.topRight.x, node.topRight.y));
  247. }
  248. }
  249. return findNode(textureAtlas, node.childNode1, image);
  250. }
  251. // If not a leaf node
  252. return findNode(textureAtlas, node.childNode1, image) ||
  253. findNode(textureAtlas, node.childNode2, image);
  254. }
  255. // Adds image of given index to the texture atlas. Called from addImage and addImages.
  256. function addImage(textureAtlas, image, index) {
  257. var node = findNode(textureAtlas, textureAtlas._root, image);
  258. if (defined(node)) {
  259. // Found a node that can hold the image.
  260. node.imageIndex = index;
  261. // Add texture coordinate and write to texture
  262. var atlasWidth = textureAtlas._texture.width;
  263. var atlasHeight = textureAtlas._texture.height;
  264. var nodeWidth = node.topRight.x - node.bottomLeft.x;
  265. var nodeHeight = node.topRight.y - node.bottomLeft.y;
  266. var x = node.bottomLeft.x / atlasWidth;
  267. var y = node.bottomLeft.y / atlasHeight;
  268. var w = nodeWidth / atlasWidth;
  269. var h = nodeHeight / atlasHeight;
  270. textureAtlas._textureCoordinates[index] = new BoundingRectangle(x, y, w, h);
  271. textureAtlas._texture.copyFrom(image, node.bottomLeft.x, node.bottomLeft.y);
  272. } else {
  273. // No node found, must resize the texture atlas.
  274. resizeAtlas(textureAtlas, image);
  275. addImage(textureAtlas, image, index);
  276. }
  277. textureAtlas._guid = createGuid();
  278. }
  279. /**
  280. * Adds an image to the atlas. If the image is already in the atlas, the atlas is unchanged and
  281. * the existing index is used.
  282. *
  283. * @param {String} id An identifier to detect whether the image already exists in the atlas.
  284. * @param {Image|Canvas|String|Resource|Promise|TextureAtlas~CreateImageCallback} image An image or canvas to add to the texture atlas,
  285. * or a URL to an Image, or a Promise for an image, or a function that creates an image.
  286. * @returns {Promise.<Number>} A Promise for the image index.
  287. */
  288. TextureAtlas.prototype.addImage = function(id, image) {
  289. //>>includeStart('debug', pragmas.debug);
  290. if (!defined(id)) {
  291. throw new DeveloperError('id is required.');
  292. }
  293. if (!defined(image)) {
  294. throw new DeveloperError('image is required.');
  295. }
  296. //>>includeEnd('debug');
  297. var indexPromise = this._idHash[id];
  298. if (defined(indexPromise)) {
  299. // we're already aware of this source
  300. return indexPromise;
  301. }
  302. // not in atlas, create the promise for the index
  303. if (typeof image === 'function') {
  304. // if image is a function, call it
  305. image = image(id);
  306. //>>includeStart('debug', pragmas.debug);
  307. if (!defined(image)) {
  308. throw new DeveloperError('image is required.');
  309. }
  310. //>>includeEnd('debug');
  311. } else if ((typeof image === 'string') || (image instanceof Resource)) {
  312. // Get a resource
  313. var resource = Resource.createIfNeeded(image);
  314. image = resource.fetchImage();
  315. }
  316. var that = this;
  317. indexPromise = when(image, function(image) {
  318. if (that.isDestroyed()) {
  319. return -1;
  320. }
  321. var index = that.numberOfImages;
  322. addImage(that, image, index);
  323. return index;
  324. });
  325. // store the promise
  326. this._idHash[id] = indexPromise;
  327. return indexPromise;
  328. };
  329. /**
  330. * Add a sub-region of an existing atlas image as additional image indices.
  331. *
  332. * @param {String} id The identifier of the existing image.
  333. * @param {BoundingRectangle} subRegion An {@link BoundingRectangle} sub-region measured in pixels from the bottom-left.
  334. *
  335. * @returns {Promise.<Number>} A Promise for the image index.
  336. */
  337. TextureAtlas.prototype.addSubRegion = function(id, subRegion) {
  338. //>>includeStart('debug', pragmas.debug);
  339. if (!defined(id)) {
  340. throw new DeveloperError('id is required.');
  341. }
  342. if (!defined(subRegion)) {
  343. throw new DeveloperError('subRegion is required.');
  344. }
  345. //>>includeEnd('debug');
  346. var indexPromise = this._idHash[id];
  347. if (!defined(indexPromise)) {
  348. throw new RuntimeError('image with id "' + id + '" not found in the atlas.');
  349. }
  350. var that = this;
  351. return when(indexPromise, function(index) {
  352. if (index === -1) {
  353. // the atlas is destroyed
  354. return -1;
  355. }
  356. var atlasWidth = that._texture.width;
  357. var atlasHeight = that._texture.height;
  358. var numImages = that.numberOfImages;
  359. var baseRegion = that._textureCoordinates[index];
  360. var x = baseRegion.x + (subRegion.x / atlasWidth);
  361. var y = baseRegion.y + (subRegion.y / atlasHeight);
  362. var w = subRegion.width / atlasWidth;
  363. var h = subRegion.height / atlasHeight;
  364. that._textureCoordinates.push(new BoundingRectangle(x, y, w, h));
  365. that._guid = createGuid();
  366. return numImages;
  367. });
  368. };
  369. /**
  370. * Returns true if this object was destroyed; otherwise, false.
  371. * <br /><br />
  372. * If this object was destroyed, it should not be used; calling any function other than
  373. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  374. *
  375. * @returns {Boolean} True if this object was destroyed; otherwise, false.
  376. *
  377. * @see TextureAtlas#destroy
  378. */
  379. TextureAtlas.prototype.isDestroyed = function() {
  380. return false;
  381. };
  382. /**
  383. * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
  384. * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
  385. * <br /><br />
  386. * Once an object is destroyed, it should not be used; calling any function other than
  387. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
  388. * assign the return value (<code>undefined</code>) to the object as done in the example.
  389. *
  390. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  391. *
  392. *
  393. * @example
  394. * atlas = atlas && atlas.destroy();
  395. *
  396. * @see TextureAtlas#isDestroyed
  397. */
  398. TextureAtlas.prototype.destroy = function() {
  399. this._texture = this._texture && this._texture.destroy();
  400. return destroyObject(this);
  401. };
  402. /**
  403. * A function that creates an image.
  404. * @callback TextureAtlas~CreateImageCallback
  405. * @param {String} id The identifier of the image to load.
  406. * @returns {Image|Promise} The image, or a promise that will resolve to an image.
  407. */
  408. export default TextureAtlas;