TextureAtlas.js 18 KB

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