GltfImageLoader.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import Check from "../Core/Check.js";
  2. import defaultValue from "../Core/defaultValue.js";
  3. import defined from "../Core/defined.js";
  4. import loadImageFromTypedArray from "../Core/loadImageFromTypedArray.js";
  5. import loadKTX2 from "../Core/loadKTX2.js";
  6. import RuntimeError from "../Core/RuntimeError.js";
  7. import ResourceLoader from "./ResourceLoader.js";
  8. import ResourceLoaderState from "./ResourceLoaderState.js";
  9. /**
  10. * Loads a glTF image.
  11. * <p>
  12. * Implements the {@link ResourceLoader} interface.
  13. * </p>
  14. *
  15. * @alias GltfImageLoader
  16. * @constructor
  17. * @augments ResourceLoader
  18. *
  19. * @param {object} options Object with the following properties:
  20. * @param {ResourceCache} options.resourceCache The {@link ResourceCache} (to avoid circular dependencies).
  21. * @param {object} options.gltf The glTF JSON.
  22. * @param {number} options.imageId The image ID.
  23. * @param {Resource} options.gltfResource The {@link Resource} containing the glTF.
  24. * @param {Resource} options.baseResource The {@link Resource} that paths in the glTF JSON are relative to.
  25. * @param {string} [options.cacheKey] The cache key of the resource.
  26. *
  27. * @private
  28. */
  29. function GltfImageLoader(options) {
  30. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  31. const resourceCache = options.resourceCache;
  32. const gltf = options.gltf;
  33. const imageId = options.imageId;
  34. const gltfResource = options.gltfResource;
  35. const baseResource = options.baseResource;
  36. const cacheKey = options.cacheKey;
  37. //>>includeStart('debug', pragmas.debug);
  38. Check.typeOf.func("options.resourceCache", resourceCache);
  39. Check.typeOf.object("options.gltf", gltf);
  40. Check.typeOf.number("options.imageId", imageId);
  41. Check.typeOf.object("options.gltfResource", gltfResource);
  42. Check.typeOf.object("options.baseResource", baseResource);
  43. //>>includeEnd('debug');
  44. const image = gltf.images[imageId];
  45. const bufferViewId = image.bufferView;
  46. const uri = image.uri;
  47. this._resourceCache = resourceCache;
  48. this._gltfResource = gltfResource;
  49. this._baseResource = baseResource;
  50. this._gltf = gltf;
  51. this._bufferViewId = bufferViewId;
  52. this._uri = uri;
  53. this._cacheKey = cacheKey;
  54. this._bufferViewLoader = undefined;
  55. this._image = undefined;
  56. this._mipLevels = undefined;
  57. this._state = ResourceLoaderState.UNLOADED;
  58. this._promise = undefined;
  59. }
  60. if (defined(Object.create)) {
  61. GltfImageLoader.prototype = Object.create(ResourceLoader.prototype);
  62. GltfImageLoader.prototype.constructor = GltfImageLoader;
  63. }
  64. Object.defineProperties(GltfImageLoader.prototype, {
  65. /**
  66. * The cache key of the resource.
  67. *
  68. * @memberof GltfImageLoader.prototype
  69. *
  70. * @type {string}
  71. * @readonly
  72. * @private
  73. */
  74. cacheKey: {
  75. get: function () {
  76. return this._cacheKey;
  77. },
  78. },
  79. /**
  80. * The image.
  81. *
  82. * @memberof GltfImageLoader.prototype
  83. *
  84. * @type {Image|ImageBitmap|CompressedTextureBuffer}
  85. * @readonly
  86. * @private
  87. */
  88. image: {
  89. get: function () {
  90. return this._image;
  91. },
  92. },
  93. /**
  94. * The mip levels. Only defined for KTX2 files containing mip levels.
  95. *
  96. * @memberof GltfImageLoader.prototype
  97. *
  98. * @type {Uint8Array[]}
  99. * @readonly
  100. * @private
  101. */
  102. mipLevels: {
  103. get: function () {
  104. return this._mipLevels;
  105. },
  106. },
  107. });
  108. /**
  109. * Loads the resource.
  110. * @returns {Promise<GltfImageLoader>} A promise which resolves to the loader when the resource loading is completed.
  111. * @private
  112. */
  113. GltfImageLoader.prototype.load = function () {
  114. if (defined(this._promise)) {
  115. return this._promise;
  116. }
  117. if (defined(this._bufferViewId)) {
  118. this._promise = loadFromBufferView(this);
  119. return this._promise;
  120. }
  121. this._promise = loadFromUri(this);
  122. return this._promise;
  123. };
  124. function getImageAndMipLevels(image) {
  125. // Images transcoded from KTX2 can contain multiple mip levels:
  126. // https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu
  127. let mipLevels;
  128. if (Array.isArray(image)) {
  129. // highest detail mip should be level 0
  130. mipLevels = image.slice(1, image.length).map(function (mipLevel) {
  131. return mipLevel.bufferView;
  132. });
  133. image = image[0];
  134. }
  135. return {
  136. image: image,
  137. mipLevels: mipLevels,
  138. };
  139. }
  140. async function loadFromBufferView(imageLoader) {
  141. imageLoader._state = ResourceLoaderState.LOADING;
  142. const resourceCache = imageLoader._resourceCache;
  143. try {
  144. const bufferViewLoader = resourceCache.getBufferViewLoader({
  145. gltf: imageLoader._gltf,
  146. bufferViewId: imageLoader._bufferViewId,
  147. gltfResource: imageLoader._gltfResource,
  148. baseResource: imageLoader._baseResource,
  149. });
  150. imageLoader._bufferViewLoader = bufferViewLoader;
  151. await bufferViewLoader.load();
  152. if (imageLoader.isDestroyed()) {
  153. return;
  154. }
  155. const typedArray = bufferViewLoader.typedArray;
  156. const image = await loadImageFromBufferTypedArray(typedArray);
  157. if (imageLoader.isDestroyed()) {
  158. return;
  159. }
  160. const imageAndMipLevels = getImageAndMipLevels(image);
  161. // Unload everything except the image
  162. imageLoader.unload();
  163. imageLoader._image = imageAndMipLevels.image;
  164. imageLoader._mipLevels = imageAndMipLevels.mipLevels;
  165. imageLoader._state = ResourceLoaderState.READY;
  166. return imageLoader;
  167. } catch (error) {
  168. if (imageLoader.isDestroyed()) {
  169. return;
  170. }
  171. return handleError(imageLoader, error, "Failed to load embedded image");
  172. }
  173. }
  174. async function loadFromUri(imageLoader) {
  175. imageLoader._state = ResourceLoaderState.LOADING;
  176. const baseResource = imageLoader._baseResource;
  177. const uri = imageLoader._uri;
  178. const resource = baseResource.getDerivedResource({
  179. url: uri,
  180. });
  181. try {
  182. const image = await loadImageFromUri(resource);
  183. if (imageLoader.isDestroyed()) {
  184. return;
  185. }
  186. const imageAndMipLevels = getImageAndMipLevels(image);
  187. // Unload everything except the image
  188. imageLoader.unload();
  189. imageLoader._image = imageAndMipLevels.image;
  190. imageLoader._mipLevels = imageAndMipLevels.mipLevels;
  191. imageLoader._state = ResourceLoaderState.READY;
  192. return imageLoader;
  193. } catch (error) {
  194. if (imageLoader.isDestroyed()) {
  195. return;
  196. }
  197. return handleError(imageLoader, error, `Failed to load image: ${uri}`);
  198. }
  199. }
  200. function handleError(imageLoader, error, errorMessage) {
  201. imageLoader.unload();
  202. imageLoader._state = ResourceLoaderState.FAILED;
  203. return Promise.reject(imageLoader.getError(errorMessage, error));
  204. }
  205. function getMimeTypeFromTypedArray(typedArray) {
  206. const header = typedArray.subarray(0, 2);
  207. const webpHeaderRIFFChars = typedArray.subarray(0, 4);
  208. const webpHeaderWEBPChars = typedArray.subarray(8, 12);
  209. if (header[0] === 0xff && header[1] === 0xd8) {
  210. // See https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
  211. return "image/jpeg";
  212. } else if (header[0] === 0x89 && header[1] === 0x50) {
  213. // See http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html
  214. return "image/png";
  215. } else if (header[0] === 0xab && header[1] === 0x4b) {
  216. // See http://github.khronos.org/KTX-Specification/#_identifier
  217. return "image/ktx2";
  218. } else if (
  219. // See https://developers.google.com/speed/webp/docs/riff_container#webp_file_header
  220. webpHeaderRIFFChars[0] === 0x52 &&
  221. webpHeaderRIFFChars[1] === 0x49 &&
  222. webpHeaderRIFFChars[2] === 0x46 &&
  223. webpHeaderRIFFChars[3] === 0x46 &&
  224. webpHeaderWEBPChars[0] === 0x57 &&
  225. webpHeaderWEBPChars[1] === 0x45 &&
  226. webpHeaderWEBPChars[2] === 0x42 &&
  227. webpHeaderWEBPChars[3] === 0x50
  228. ) {
  229. return "image/webp";
  230. }
  231. throw new RuntimeError("Image format is not recognized");
  232. }
  233. async function loadImageFromBufferTypedArray(typedArray) {
  234. const mimeType = getMimeTypeFromTypedArray(typedArray);
  235. if (mimeType === "image/ktx2") {
  236. // Need to make a copy of the embedded KTX2 buffer otherwise the underlying
  237. // ArrayBuffer may be accessed on both the worker and the main thread and
  238. // throw an error like "Cannot perform Construct on a detached ArrayBuffer".
  239. // Look into SharedArrayBuffer at some point to get around this.
  240. const ktxBuffer = new Uint8Array(typedArray);
  241. // Resolves to a CompressedTextureBuffer
  242. return loadKTX2(ktxBuffer);
  243. }
  244. // Resolves to an Image or ImageBitmap
  245. return GltfImageLoader._loadImageFromTypedArray({
  246. uint8Array: typedArray,
  247. format: mimeType,
  248. flipY: false,
  249. skipColorSpaceConversion: true,
  250. });
  251. }
  252. const ktx2Regex = /(^data:image\/ktx2)|(\.ktx2$)/i;
  253. function loadImageFromUri(resource) {
  254. const uri = resource.getUrlComponent(false, true);
  255. if (ktx2Regex.test(uri)) {
  256. // Resolves to a CompressedTextureBuffer
  257. return loadKTX2(resource);
  258. }
  259. // Resolves to an ImageBitmap or Image
  260. return resource.fetchImage({
  261. skipColorSpaceConversion: true,
  262. preferImageBitmap: true,
  263. });
  264. }
  265. /**
  266. * Unloads the resource.
  267. * @private
  268. */
  269. GltfImageLoader.prototype.unload = function () {
  270. if (
  271. defined(this._bufferViewLoader) &&
  272. !this._bufferViewLoader.isDestroyed()
  273. ) {
  274. this._resourceCache.unload(this._bufferViewLoader);
  275. }
  276. this._bufferViewLoader = undefined;
  277. this._uri = undefined; // Free in case the uri is a data uri
  278. this._image = undefined;
  279. this._mipLevels = undefined;
  280. this._gltf = undefined;
  281. };
  282. // Exposed for testing
  283. GltfImageLoader._loadImageFromTypedArray = loadImageFromTypedArray;
  284. export default GltfImageLoader;