TextureManager.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import defined from "../../Core/defined.js";
  2. import destroyObject from "../../Core/destroyObject.js";
  3. import getImageFromTypedArray from "../../Core/getImageFromTypedArray.js";
  4. import CesiumMath from "../../Core/Math.js";
  5. import resizeImageToNextPowerOfTwo from "../../Core/resizeImageToNextPowerOfTwo.js";
  6. import PixelDatatype from "../../Renderer/PixelDatatype.js";
  7. import Texture from "../../Renderer/Texture.js";
  8. import TextureMinificationFilter from "../../Renderer/TextureMinificationFilter.js";
  9. import TextureWrap from "../../Renderer/TextureWrap.js";
  10. /**
  11. * An object to manage loading textures
  12. *
  13. * @alias TextureManager
  14. * @constructor
  15. *
  16. * @private
  17. * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
  18. */
  19. function TextureManager() {
  20. this._defaultTexture = undefined;
  21. this._textures = {};
  22. this._loadedImages = [];
  23. // Keep track of the last time update() was called to avoid
  24. // calling update() twice.
  25. this._lastUpdatedFrame = -1;
  26. }
  27. /**
  28. * Get one of the loaded textures
  29. * @param {string} textureId The unique ID of the texture loaded by {@link TextureManager#loadTexture2D}
  30. * @return {Texture} The texture or <code>undefined</code> if no texture exists
  31. */
  32. TextureManager.prototype.getTexture = function (textureId) {
  33. return this._textures[textureId];
  34. };
  35. function fetchTexture2D(textureManager, textureId, textureUniform) {
  36. textureUniform.resource
  37. .fetchImage()
  38. .then(function (image) {
  39. textureManager._loadedImages.push({
  40. id: textureId,
  41. image: image,
  42. textureUniform: textureUniform,
  43. });
  44. })
  45. .catch(function () {
  46. const texture = textureManager._textures[textureId];
  47. if (defined(texture) && texture !== textureManager._defaultTexture) {
  48. texture.destroy();
  49. }
  50. textureManager._textures[textureId] = textureManager._defaultTexture;
  51. });
  52. }
  53. /**
  54. * Load a texture 2D asynchronously. Note that {@link TextureManager#update}
  55. * must be called in the render loop to finish processing the textures.
  56. *
  57. * @param {string} textureId A unique ID to identify this texture.
  58. * @param {TextureUniform} textureUniform A description of the texture
  59. *
  60. * @private
  61. */
  62. TextureManager.prototype.loadTexture2D = function (textureId, textureUniform) {
  63. if (defined(textureUniform.typedArray)) {
  64. this._loadedImages.push({
  65. id: textureId,
  66. textureUniform: textureUniform,
  67. });
  68. } else {
  69. fetchTexture2D(this, textureId, textureUniform);
  70. }
  71. };
  72. function createTexture(textureManager, loadedImage, context) {
  73. const { id, textureUniform, image } = loadedImage;
  74. // If the context is WebGL1, and the sampler needs mipmaps or repeating
  75. // boundary conditions, the image may need to be resized first
  76. const texture = context.webgl2
  77. ? getTextureAndMips(textureUniform, image, context)
  78. : getWebGL1Texture(textureUniform, image, context);
  79. // Destroy the old texture once the new one is loaded for more seamless
  80. // transitions between values
  81. const oldTexture = textureManager._textures[id];
  82. if (defined(oldTexture) && oldTexture !== context.defaultTexture) {
  83. oldTexture.destroy();
  84. }
  85. textureManager._textures[id] = texture;
  86. }
  87. function getTextureAndMips(textureUniform, image, context) {
  88. const { typedArray, sampler } = textureUniform;
  89. const texture = defined(typedArray)
  90. ? getTextureFromTypedArray(textureUniform, context)
  91. : new Texture({ context, source: image, sampler });
  92. if (samplerRequiresMipmap(sampler)) {
  93. texture.generateMipmap();
  94. }
  95. return texture;
  96. }
  97. function getWebGL1Texture(textureUniform, image, context) {
  98. const { typedArray, sampler } = textureUniform;
  99. // WebGL1 requires power-of-two texture dimensions for mipmapping and REPEAT wrap modes
  100. const needMipmap = samplerRequiresMipmap(sampler);
  101. const samplerRepeats =
  102. sampler.wrapS === TextureWrap.REPEAT ||
  103. sampler.wrapS === TextureWrap.MIRRORED_REPEAT ||
  104. sampler.wrapT === TextureWrap.REPEAT ||
  105. sampler.wrapT === TextureWrap.MIRRORED_REPEAT;
  106. const { width, height } = defined(typedArray) ? textureUniform : image;
  107. const isPowerOfTwo = [width, height].every(CesiumMath.isPowerOfTwo);
  108. const requiresResize = (needMipmap || samplerRepeats) && !isPowerOfTwo;
  109. if (!requiresResize) {
  110. return getTextureAndMips(textureUniform, image, context);
  111. } else if (!defined(typedArray)) {
  112. const resizedImage = resizeImageToNextPowerOfTwo(image);
  113. return getTextureAndMips(textureUniform, resizedImage, context);
  114. } else if (textureUniform.pixelDatatype === PixelDatatype.UNSIGNED_BYTE) {
  115. const imageFromArray = getImageFromTypedArray(typedArray, width, height);
  116. const resizedImage = resizeImageToNextPowerOfTwo(imageFromArray);
  117. return getTextureAndMips({ sampler }, resizedImage, context);
  118. }
  119. // typedArray is non-power-of-two but can't be resized. Warn and return raw texture (no mipmaps)
  120. if (needMipmap) {
  121. console.warn(
  122. "Texture requires resizing for mipmaps but pixelDataType cannot be resized. The texture may be rendered incorrectly."
  123. );
  124. } else if (samplerRepeats) {
  125. console.warn(
  126. "Texture requires resizing for wrapping but pixelDataType cannot be resized. The texture may be rendered incorrectly."
  127. );
  128. }
  129. return getTextureFromTypedArray(textureUniform, context);
  130. }
  131. function samplerRequiresMipmap(sampler) {
  132. return [
  133. TextureMinificationFilter.NEAREST_MIPMAP_NEAREST,
  134. TextureMinificationFilter.NEAREST_MIPMAP_LINEAR,
  135. TextureMinificationFilter.LINEAR_MIPMAP_NEAREST,
  136. TextureMinificationFilter.LINEAR_MIPMAP_LINEAR,
  137. ].includes(sampler.minificationFilter);
  138. }
  139. function getTextureFromTypedArray(textureUniform, context) {
  140. const {
  141. pixelFormat,
  142. pixelDatatype,
  143. width,
  144. height,
  145. typedArray: arrayBufferView,
  146. sampler,
  147. } = textureUniform;
  148. return new Texture({
  149. context,
  150. pixelFormat,
  151. pixelDatatype,
  152. source: { arrayBufferView, width, height },
  153. sampler,
  154. flipY: false,
  155. });
  156. }
  157. TextureManager.prototype.update = function (frameState) {
  158. // update only needs to be called once a frame.
  159. if (frameState.frameNumber === this._lastUpdatedFrame) {
  160. return;
  161. }
  162. this._lastUpdatedFrame = frameState.frameNumber;
  163. const context = frameState.context;
  164. this._defaultTexture = context.defaultTexture;
  165. // If any images were loaded since the last frame, create Textures
  166. // for them and store in the uniform dictionary
  167. const loadedImages = this._loadedImages;
  168. for (let i = 0; i < loadedImages.length; i++) {
  169. const loadedImage = loadedImages[i];
  170. createTexture(this, loadedImage, context);
  171. }
  172. loadedImages.length = 0;
  173. };
  174. /**
  175. * Returns true if this object was destroyed; otherwise, false.
  176. * <br /><br />
  177. * If this object was destroyed, it should not be used; calling any function other than
  178. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  179. *
  180. * @returns {boolean} True if this object was destroyed; otherwise, false.
  181. *
  182. * @see TextureManager#destroy
  183. * @private
  184. */
  185. TextureManager.prototype.isDestroyed = function () {
  186. return false;
  187. };
  188. /**
  189. * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
  190. * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
  191. * <br /><br />
  192. * Once an object is destroyed, it should not be used; calling any function other than
  193. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
  194. * assign the return value (<code>undefined</code>) to the object as done in the example.
  195. *
  196. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  197. *
  198. * @example
  199. * textureManager = textureManager && textureManager.destroy();
  200. *
  201. * @see TextureManager#isDestroyed
  202. * @private
  203. */
  204. TextureManager.prototype.destroy = function () {
  205. const textures = this._textures;
  206. for (const texture in textures) {
  207. if (textures.hasOwnProperty(texture)) {
  208. const instance = textures[texture];
  209. if (instance !== this._defaultTexture) {
  210. instance.destroy();
  211. }
  212. }
  213. }
  214. return destroyObject(this);
  215. };
  216. export default TextureManager;