123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- import Check from "../Core/Check.js";
- import defaultValue from "../Core/defaultValue.js";
- import defer from "../Core/defer.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
- */
- export default function GltfImageLoader(options) {
- options = defaultValue(options, defaultValue.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 = defer();
- }
- if (defined(Object.create)) {
- GltfImageLoader.prototype = Object.create(ResourceLoader.prototype);
- GltfImageLoader.prototype.constructor = GltfImageLoader;
- }
- Object.defineProperties(GltfImageLoader.prototype, {
- /**
- * A promise that resolves to the resource when the resource is ready.
- *
- * @memberof GltfImageLoader.prototype
- *
- * @type {Promise.<GltfImageLoader>}
- * @readonly
- * @private
- */
- promise: {
- get: function () {
- return this._promise.promise;
- },
- },
- /**
- * 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.
- * @private
- */
- GltfImageLoader.prototype.load = function () {
- if (defined(this._bufferViewId)) {
- loadFromBufferView(this);
- } else {
- loadFromUri(this);
- }
- };
- 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;
- if (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,
- };
- }
- function loadFromBufferView(imageLoader) {
- const resourceCache = imageLoader._resourceCache;
- const bufferViewLoader = resourceCache.loadBufferView({
- gltf: imageLoader._gltf,
- bufferViewId: imageLoader._bufferViewId,
- gltfResource: imageLoader._gltfResource,
- baseResource: imageLoader._baseResource,
- });
- imageLoader._bufferViewLoader = bufferViewLoader;
- imageLoader._state = ResourceLoaderState.LOADING;
- bufferViewLoader.promise
- .then(function () {
- if (imageLoader.isDestroyed()) {
- return;
- }
- const typedArray = bufferViewLoader.typedArray;
- return loadImageFromBufferTypedArray(typedArray).then(function (image) {
- 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;
- imageLoader._promise.resolve(imageLoader);
- });
- })
- .catch(function (error) {
- if (imageLoader.isDestroyed()) {
- return;
- }
- handleError(imageLoader, error, "Failed to load embedded image");
- });
- }
- function loadFromUri(imageLoader) {
- const baseResource = imageLoader._baseResource;
- const uri = imageLoader._uri;
- const resource = baseResource.getDerivedResource({
- url: uri,
- });
- imageLoader._state = ResourceLoaderState.LOADING;
- loadImageFromUri(resource)
- .then(function (image) {
- 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;
- imageLoader._promise.resolve(imageLoader);
- })
- .catch(function (error) {
- if (imageLoader.isDestroyed()) {
- return;
- }
- handleError(imageLoader, error, `Failed to load image: ${uri}`);
- });
- }
- function handleError(imageLoader, error, errorMessage) {
- imageLoader.unload();
- imageLoader._state = ResourceLoaderState.FAILED;
- imageLoader._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 if (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");
- }
- function loadImageFromBufferTypedArray(typedArray) {
- const mimeType = getMimeTypeFromTypedArray(typedArray);
- if (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.url;
- if (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._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;
|