import Check from "../Core/Check.js";
import CesiumMath from "../Core/Math.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import PixelFormat from "../Core/PixelFormat.js";
import Texture from "../Renderer/Texture.js";
import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js";
import TextureWrap from "../Renderer/TextureWrap.js";
import GltfLoaderUtil from "./GltfLoaderUtil.js";
import JobType from "./JobType.js";
import ResourceLoader from "./ResourceLoader.js";
import ResourceLoaderState from "./ResourceLoaderState.js";
import resizeImageToNextPowerOfTwo from "../Core/resizeImageToNextPowerOfTwo.js";
/**
* Loads a glTF texture.
*
* Implements the {@link ResourceLoader} interface.
*
*
* @alias GltfTextureLoader
* @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 {object} options.textureInfo The texture info object.
* @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 {SupportedImageFormats} options.supportedImageFormats The supported image formats.
* @param {string} [options.cacheKey] The cache key of the resource.
* @param {boolean} [options.asynchronous=true] Determines if WebGL resource creation will be spread out over several frames or block until all WebGL resources are created.
*
* @private
*/
function GltfTextureLoader(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const resourceCache = options.resourceCache;
const gltf = options.gltf;
const textureInfo = options.textureInfo;
const gltfResource = options.gltfResource;
const baseResource = options.baseResource;
const supportedImageFormats = options.supportedImageFormats;
const cacheKey = options.cacheKey;
const asynchronous = defaultValue(options.asynchronous, true);
//>>includeStart('debug', pragmas.debug);
Check.typeOf.func("options.resourceCache", resourceCache);
Check.typeOf.object("options.gltf", gltf);
Check.typeOf.object("options.textureInfo", textureInfo);
Check.typeOf.object("options.gltfResource", gltfResource);
Check.typeOf.object("options.baseResource", baseResource);
Check.typeOf.object("options.supportedImageFormats", supportedImageFormats);
//>>includeEnd('debug');
const textureId = textureInfo.index;
// imageId is guaranteed to be defined otherwise the GltfTextureLoader
// wouldn't have been created
const imageId = GltfLoaderUtil.getImageIdFromTexture({
gltf: gltf,
textureId: textureId,
supportedImageFormats: supportedImageFormats,
});
this._resourceCache = resourceCache;
this._gltf = gltf;
this._textureInfo = textureInfo;
this._imageId = imageId;
this._gltfResource = gltfResource;
this._baseResource = baseResource;
this._cacheKey = cacheKey;
this._asynchronous = asynchronous;
this._imageLoader = undefined;
this._image = undefined;
this._mipLevels = undefined;
this._texture = undefined;
this._state = ResourceLoaderState.UNLOADED;
this._promise = undefined;
}
if (defined(Object.create)) {
GltfTextureLoader.prototype = Object.create(ResourceLoader.prototype);
GltfTextureLoader.prototype.constructor = GltfTextureLoader;
}
Object.defineProperties(GltfTextureLoader.prototype, {
/**
* The cache key of the resource.
*
* @memberof GltfTextureLoader.prototype
*
* @type {string}
* @readonly
* @private
*/
cacheKey: {
get: function () {
return this._cacheKey;
},
},
/**
* The texture.
*
* @memberof GltfTextureLoader.prototype
*
* @type {Texture}
* @readonly
* @private
*/
texture: {
get: function () {
return this._texture;
},
},
});
const scratchTextureJob = new CreateTextureJob();
async function loadResources(loader) {
const resourceCache = loader._resourceCache;
try {
const imageLoader = resourceCache.getImageLoader({
gltf: loader._gltf,
imageId: loader._imageId,
gltfResource: loader._gltfResource,
baseResource: loader._baseResource,
});
loader._imageLoader = imageLoader;
await imageLoader.load();
if (loader.isDestroyed()) {
return;
}
// Now wait for process() to run to finish loading
loader._image = imageLoader.image;
loader._mipLevels = imageLoader.mipLevels;
loader._state = ResourceLoaderState.LOADED;
return loader;
} catch (error) {
if (loader.isDestroyed()) {
return;
}
loader.unload();
loader._state = ResourceLoaderState.FAILED;
const errorMessage = "Failed to load texture";
throw loader.getError(errorMessage, error);
}
}
/**
* Loads the resource.
* @returns {Promise} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
GltfTextureLoader.prototype.load = async function () {
if (defined(this._promise)) {
return this._promise;
}
this._state = ResourceLoaderState.LOADING;
this._promise = loadResources(this);
return this._promise;
};
function CreateTextureJob() {
this.gltf = undefined;
this.textureInfo = undefined;
this.image = undefined;
this.context = undefined;
this.texture = undefined;
}
CreateTextureJob.prototype.set = function (
gltf,
textureInfo,
image,
mipLevels,
context
) {
this.gltf = gltf;
this.textureInfo = textureInfo;
this.image = image;
this.mipLevels = mipLevels;
this.context = context;
};
CreateTextureJob.prototype.execute = function () {
this.texture = createTexture(
this.gltf,
this.textureInfo,
this.image,
this.mipLevels,
this.context
);
};
function createTexture(gltf, textureInfo, image, mipLevels, context) {
// internalFormat is only defined for CompressedTextureBuffer
const internalFormat = image.internalFormat;
let compressedTextureNoMipmap = false;
if (PixelFormat.isCompressedFormat(internalFormat) && !defined(mipLevels)) {
compressedTextureNoMipmap = true;
}
const sampler = GltfLoaderUtil.createSampler({
gltf: gltf,
textureInfo: textureInfo,
compressedTextureNoMipmap: compressedTextureNoMipmap,
});
const minFilter = sampler.minificationFilter;
const wrapS = sampler.wrapS;
const wrapT = sampler.wrapT;
const samplerRequiresMipmap =
minFilter === TextureMinificationFilter.NEAREST_MIPMAP_NEAREST ||
minFilter === TextureMinificationFilter.NEAREST_MIPMAP_LINEAR ||
minFilter === TextureMinificationFilter.LINEAR_MIPMAP_NEAREST ||
minFilter === TextureMinificationFilter.LINEAR_MIPMAP_LINEAR;
// generateMipmap is disallowed for compressed textures. Compressed textures
// can have mipmaps but they must come with the KTX2 instead of generated by
// WebGL. Also note from the KHR_texture_basisu spec:
//
// When a texture refers to a sampler with mipmap minification or when the
// sampler is undefined, the KTX2 image SHOULD contain a full mip pyramid.
//
const generateMipmap = !defined(internalFormat) && samplerRequiresMipmap;
// WebGL 1 requires power-of-two texture dimensions for mipmapping and REPEAT/MIRRORED_REPEAT wrap modes.
const requiresPowerOfTwo =
generateMipmap ||
wrapS === TextureWrap.REPEAT ||
wrapS === TextureWrap.MIRRORED_REPEAT ||
wrapT === TextureWrap.REPEAT ||
wrapT === TextureWrap.MIRRORED_REPEAT;
const nonPowerOfTwo =
!CesiumMath.isPowerOfTwo(image.width) ||
!CesiumMath.isPowerOfTwo(image.height);
const requiresResize = requiresPowerOfTwo && nonPowerOfTwo;
let texture;
if (defined(internalFormat)) {
if (
!context.webgl2 &&
PixelFormat.isCompressedFormat(internalFormat) &&
nonPowerOfTwo &&
requiresPowerOfTwo
) {
console.warn(
"Compressed texture uses REPEAT or MIRRORED_REPEAT texture wrap mode and dimensions are not powers of two. The texture may be rendered incorrectly."
);
}
texture = Texture.create({
context: context,
source: {
arrayBufferView: image.bufferView, // Only defined for CompressedTextureBuffer
mipLevels: mipLevels,
},
width: image.width,
height: image.height,
pixelFormat: image.internalFormat, // Only defined for CompressedTextureBuffer
sampler: sampler,
});
} else {
if (requiresResize) {
image = resizeImageToNextPowerOfTwo(image);
}
texture = Texture.create({
context: context,
source: image,
sampler: sampler,
flipY: false,
skipColorSpaceConversion: true,
});
}
if (generateMipmap) {
texture.generateMipmap();
}
return texture;
}
/**
* Processes the resource until it becomes ready.
*
* @param {FrameState} frameState The frame state.
* @returns {boolean} true once all resourced are ready.
* @private
*/
GltfTextureLoader.prototype.process = function (frameState) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("frameState", frameState);
//>>includeEnd('debug');
if (this._state === ResourceLoaderState.READY) {
return true;
}
if (
this._state !== ResourceLoaderState.LOADED &&
this._state !== ResourceLoaderState.PROCESSING
) {
return false;
}
if (defined(this._texture)) {
// Already created texture
return false;
}
if (!defined(this._image)) {
// Not ready to create texture
return false;
}
this._state = ResourceLoaderState.PROCESSING;
let texture;
if (this._asynchronous) {
const textureJob = scratchTextureJob;
textureJob.set(
this._gltf,
this._textureInfo,
this._image,
this._mipLevels,
frameState.context
);
const jobScheduler = frameState.jobScheduler;
if (!jobScheduler.execute(textureJob, JobType.TEXTURE)) {
// Job scheduler is full. Try again next frame.
return;
}
texture = textureJob.texture;
} else {
texture = createTexture(
this._gltf,
this._textureInfo,
this._image,
this._mipLevels,
frameState.context
);
}
// Unload everything except the texture
this.unload();
this._texture = texture;
this._state = ResourceLoaderState.READY;
this._resourceCache.statistics.addTextureLoader(this);
return true;
};
/**
* Unloads the resource.
* @private
*/
GltfTextureLoader.prototype.unload = function () {
if (defined(this._texture)) {
this._texture.destroy();
}
if (defined(this._imageLoader) && !this._imageLoader.isDestroyed()) {
this._resourceCache.unload(this._imageLoader);
}
this._imageLoader = undefined;
this._image = undefined;
this._mipLevels = undefined;
this._texture = undefined;
this._gltf = undefined;
};
export default GltfTextureLoader;