GltfImageLoader.js 9.3 KB

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