All files / engine/Source/Scene GltfImageLoader.js

92.43% Statements 110/119
82% Branches 41/50
92.85% Functions 13/14
92.43% Lines 110/119

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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