import Check from "../Core/Check.js";
|
import defaultValue from "../Core/defaultValue.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 when from "../ThirdParty/when.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);
|
var resourceCache = options.resourceCache;
|
var gltf = options.gltf;
|
var imageId = options.imageId;
|
var gltfResource = options.gltfResource;
|
var baseResource = options.baseResource;
|
var 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');
|
|
var image = gltf.images[imageId];
|
var bufferViewId = image.bufferView;
|
var 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 = when.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
|
var 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) {
|
var resourceCache = imageLoader._resourceCache;
|
var 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;
|
}
|
|
var typedArray = bufferViewLoader.typedArray;
|
return loadImageFromBufferTypedArray(typedArray).then(function (image) {
|
if (imageLoader.isDestroyed()) {
|
return;
|
}
|
|
var 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);
|
});
|
})
|
.otherwise(function (error) {
|
if (imageLoader.isDestroyed()) {
|
return;
|
}
|
handleError(imageLoader, error, "Failed to load embedded image");
|
});
|
}
|
|
function loadFromUri(imageLoader) {
|
var baseResource = imageLoader._baseResource;
|
var uri = imageLoader._uri;
|
var resource = baseResource.getDerivedResource({
|
url: uri,
|
});
|
imageLoader._state = ResourceLoaderState.LOADING;
|
loadImageFromUri(resource)
|
.then(function (image) {
|
if (imageLoader.isDestroyed()) {
|
return;
|
}
|
|
var 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);
|
})
|
.otherwise(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) {
|
var header = typedArray.subarray(0, 2);
|
var webpHeaderRIFFChars = typedArray.subarray(0, 4);
|
var 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) {
|
var 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.
|
var 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,
|
});
|
}
|
|
var ktx2Regex = /(^data:image\/ktx2)|(\.ktx2$)/i;
|
|
function loadImageFromUri(resource) {
|
var 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;
|