GltfTextureLoader.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. import Check from "../Core/Check.js";
  2. import CesiumMath from "../Core/Math.js";
  3. import defaultValue from "../Core/defaultValue.js";
  4. import defer from "../Core/defer.js";
  5. import defined from "../Core/defined.js";
  6. import PixelFormat from "../Core/PixelFormat.js";
  7. import Texture from "../Renderer/Texture.js";
  8. import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js";
  9. import TextureWrap from "../Renderer/TextureWrap.js";
  10. import GltfLoaderUtil from "./GltfLoaderUtil.js";
  11. import JobType from "./JobType.js";
  12. import ResourceLoader from "./ResourceLoader.js";
  13. import ResourceLoaderState from "./ResourceLoaderState.js";
  14. /**
  15. * Loads a glTF texture.
  16. * <p>
  17. * Implements the {@link ResourceLoader} interface.
  18. * </p>
  19. *
  20. * @alias GltfTextureLoader
  21. * @constructor
  22. * @augments ResourceLoader
  23. *
  24. * @param {Object} options Object with the following properties:
  25. * @param {ResourceCache} options.resourceCache The {@link ResourceCache} (to avoid circular dependencies).
  26. * @param {Object} options.gltf The glTF JSON.
  27. * @param {Object} options.textureInfo The texture info object.
  28. * @param {Resource} options.gltfResource The {@link Resource} containing the glTF.
  29. * @param {Resource} options.baseResource The {@link Resource} that paths in the glTF JSON are relative to.
  30. * @param {SupportedImageFormats} options.supportedImageFormats The supported image formats.
  31. * @param {String} [options.cacheKey] The cache key of the resource.
  32. * @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.
  33. *
  34. * @private
  35. */
  36. export default function GltfTextureLoader(options) {
  37. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  38. const resourceCache = options.resourceCache;
  39. const gltf = options.gltf;
  40. const textureInfo = options.textureInfo;
  41. const gltfResource = options.gltfResource;
  42. const baseResource = options.baseResource;
  43. const supportedImageFormats = options.supportedImageFormats;
  44. const cacheKey = options.cacheKey;
  45. const asynchronous = defaultValue(options.asynchronous, true);
  46. //>>includeStart('debug', pragmas.debug);
  47. Check.typeOf.func("options.resourceCache", resourceCache);
  48. Check.typeOf.object("options.gltf", gltf);
  49. Check.typeOf.object("options.textureInfo", textureInfo);
  50. Check.typeOf.object("options.gltfResource", gltfResource);
  51. Check.typeOf.object("options.baseResource", baseResource);
  52. Check.typeOf.object("options.supportedImageFormats", supportedImageFormats);
  53. //>>includeEnd('debug');
  54. const textureId = textureInfo.index;
  55. // imageId is guaranteed to be defined otherwise the GltfTextureLoader
  56. // wouldn't have been created
  57. const imageId = GltfLoaderUtil.getImageIdFromTexture({
  58. gltf: gltf,
  59. textureId: textureId,
  60. supportedImageFormats: supportedImageFormats,
  61. });
  62. this._resourceCache = resourceCache;
  63. this._gltf = gltf;
  64. this._textureInfo = textureInfo;
  65. this._imageId = imageId;
  66. this._gltfResource = gltfResource;
  67. this._baseResource = baseResource;
  68. this._cacheKey = cacheKey;
  69. this._asynchronous = asynchronous;
  70. this._imageLoader = undefined;
  71. this._image = undefined;
  72. this._mipLevels = undefined;
  73. this._texture = undefined;
  74. this._state = ResourceLoaderState.UNLOADED;
  75. this._promise = defer();
  76. }
  77. if (defined(Object.create)) {
  78. GltfTextureLoader.prototype = Object.create(ResourceLoader.prototype);
  79. GltfTextureLoader.prototype.constructor = GltfTextureLoader;
  80. }
  81. Object.defineProperties(GltfTextureLoader.prototype, {
  82. /**
  83. * A promise that resolves to the resource when the resource is ready.
  84. *
  85. * @memberof GltfTextureLoader.prototype
  86. *
  87. * @type {Promise.<GltfTextureLoader>}
  88. * @readonly
  89. * @private
  90. */
  91. promise: {
  92. get: function () {
  93. return this._promise.promise;
  94. },
  95. },
  96. /**
  97. * The cache key of the resource.
  98. *
  99. * @memberof GltfTextureLoader.prototype
  100. *
  101. * @type {String}
  102. * @readonly
  103. * @private
  104. */
  105. cacheKey: {
  106. get: function () {
  107. return this._cacheKey;
  108. },
  109. },
  110. /**
  111. * The texture.
  112. *
  113. * @memberof GltfTextureLoader.prototype
  114. *
  115. * @type {Texture}
  116. * @readonly
  117. * @private
  118. */
  119. texture: {
  120. get: function () {
  121. return this._texture;
  122. },
  123. },
  124. });
  125. /**
  126. * Loads the resource.
  127. * @private
  128. */
  129. GltfTextureLoader.prototype.load = function () {
  130. const resourceCache = this._resourceCache;
  131. const imageLoader = resourceCache.loadImage({
  132. gltf: this._gltf,
  133. imageId: this._imageId,
  134. gltfResource: this._gltfResource,
  135. baseResource: this._baseResource,
  136. });
  137. this._imageLoader = imageLoader;
  138. this._state = ResourceLoaderState.LOADING;
  139. const that = this;
  140. imageLoader.promise
  141. .then(function () {
  142. if (that.isDestroyed()) {
  143. return;
  144. }
  145. // Now wait for process() to run to finish loading
  146. that._image = imageLoader.image;
  147. that._mipLevels = imageLoader.mipLevels;
  148. that._state = ResourceLoaderState.PROCESSING;
  149. })
  150. .catch(function (error) {
  151. if (that.isDestroyed()) {
  152. return;
  153. }
  154. that.unload();
  155. that._state = ResourceLoaderState.FAILED;
  156. const errorMessage = "Failed to load texture";
  157. that._promise.reject(that.getError(errorMessage, error));
  158. });
  159. };
  160. function CreateTextureJob() {
  161. this.gltf = undefined;
  162. this.textureInfo = undefined;
  163. this.image = undefined;
  164. this.context = undefined;
  165. this.texture = undefined;
  166. }
  167. CreateTextureJob.prototype.set = function (
  168. gltf,
  169. textureInfo,
  170. image,
  171. mipLevels,
  172. context
  173. ) {
  174. this.gltf = gltf;
  175. this.textureInfo = textureInfo;
  176. this.image = image;
  177. this.mipLevels = mipLevels;
  178. this.context = context;
  179. };
  180. CreateTextureJob.prototype.execute = function () {
  181. this.texture = createTexture(
  182. this.gltf,
  183. this.textureInfo,
  184. this.image,
  185. this.mipLevels,
  186. this.context
  187. );
  188. };
  189. function resizeImageToNextPowerOfTwo(image) {
  190. const canvas = document.createElement("canvas");
  191. canvas.width = CesiumMath.nextPowerOfTwo(image.width);
  192. canvas.height = CesiumMath.nextPowerOfTwo(image.height);
  193. const canvasContext = canvas.getContext("2d");
  194. canvasContext.drawImage(
  195. image,
  196. 0,
  197. 0,
  198. image.width,
  199. image.height,
  200. 0,
  201. 0,
  202. canvas.width,
  203. canvas.height
  204. );
  205. return canvas;
  206. }
  207. function createTexture(gltf, textureInfo, image, mipLevels, context) {
  208. // internalFormat is only defined for CompressedTextureBuffer
  209. const internalFormat = image.internalFormat;
  210. let compressedTextureNoMipmap = false;
  211. if (PixelFormat.isCompressedFormat(internalFormat) && !defined(mipLevels)) {
  212. compressedTextureNoMipmap = true;
  213. }
  214. const sampler = GltfLoaderUtil.createSampler({
  215. gltf: gltf,
  216. textureInfo: textureInfo,
  217. compressedTextureNoMipmap: compressedTextureNoMipmap,
  218. });
  219. const minFilter = sampler.minificationFilter;
  220. const wrapS = sampler.wrapS;
  221. const wrapT = sampler.wrapT;
  222. const samplerRequiresMipmap =
  223. minFilter === TextureMinificationFilter.NEAREST_MIPMAP_NEAREST ||
  224. minFilter === TextureMinificationFilter.NEAREST_MIPMAP_LINEAR ||
  225. minFilter === TextureMinificationFilter.LINEAR_MIPMAP_NEAREST ||
  226. minFilter === TextureMinificationFilter.LINEAR_MIPMAP_LINEAR;
  227. // generateMipmap is disallowed for compressed textures. Compressed textures
  228. // can have mipmaps but they must come with the KTX2 instead of generated by
  229. // WebGL. Also note from the KHR_texture_basisu spec:
  230. //
  231. // When a texture refers to a sampler with mipmap minification or when the
  232. // sampler is undefined, the KTX2 image SHOULD contain a full mip pyramid.
  233. //
  234. const generateMipmap = !defined(internalFormat) && samplerRequiresMipmap;
  235. // WebGL 1 requires power-of-two texture dimensions for mipmapping and REPEAT/MIRRORED_REPEAT wrap modes.
  236. const requiresPowerOfTwo =
  237. generateMipmap ||
  238. wrapS === TextureWrap.REPEAT ||
  239. wrapS === TextureWrap.MIRRORED_REPEAT ||
  240. wrapT === TextureWrap.REPEAT ||
  241. wrapT === TextureWrap.MIRRORED_REPEAT;
  242. const nonPowerOfTwo =
  243. !CesiumMath.isPowerOfTwo(image.width) ||
  244. !CesiumMath.isPowerOfTwo(image.height);
  245. const requiresResize = requiresPowerOfTwo && nonPowerOfTwo;
  246. let texture;
  247. if (defined(internalFormat)) {
  248. if (
  249. !context.webgl2 &&
  250. PixelFormat.isCompressedFormat(internalFormat) &&
  251. nonPowerOfTwo &&
  252. requiresPowerOfTwo
  253. ) {
  254. console.warn(
  255. "Compressed texture uses REPEAT or MIRRORED_REPEAT texture wrap mode and dimensions are not powers of two. The texture may be rendered incorrectly."
  256. );
  257. }
  258. texture = Texture.create({
  259. context: context,
  260. source: {
  261. arrayBufferView: image.bufferView, // Only defined for CompressedTextureBuffer
  262. mipLevels: mipLevels,
  263. },
  264. width: image.width,
  265. height: image.height,
  266. pixelFormat: image.internalFormat, // Only defined for CompressedTextureBuffer
  267. sampler: sampler,
  268. });
  269. } else {
  270. if (requiresResize) {
  271. image = resizeImageToNextPowerOfTwo(image);
  272. }
  273. texture = Texture.create({
  274. context: context,
  275. source: image,
  276. sampler: sampler,
  277. flipY: false,
  278. skipColorSpaceConversion: true,
  279. });
  280. }
  281. if (generateMipmap) {
  282. texture.generateMipmap();
  283. }
  284. return texture;
  285. }
  286. const scratchTextureJob = new CreateTextureJob();
  287. /**
  288. * Processes the resource until it becomes ready.
  289. *
  290. * @param {FrameState} frameState The frame state.
  291. * @private
  292. */
  293. GltfTextureLoader.prototype.process = function (frameState) {
  294. //>>includeStart('debug', pragmas.debug);
  295. Check.typeOf.object("frameState", frameState);
  296. //>>includeEnd('debug');
  297. if (defined(this._texture)) {
  298. // Already created texture
  299. return;
  300. }
  301. if (!defined(this._image)) {
  302. // Not ready to create texture
  303. return;
  304. }
  305. let texture;
  306. if (this._asynchronous) {
  307. const textureJob = scratchTextureJob;
  308. textureJob.set(
  309. this._gltf,
  310. this._textureInfo,
  311. this._image,
  312. this._mipLevels,
  313. frameState.context
  314. );
  315. const jobScheduler = frameState.jobScheduler;
  316. if (!jobScheduler.execute(textureJob, JobType.TEXTURE)) {
  317. // Job scheduler is full. Try again next frame.
  318. return;
  319. }
  320. texture = textureJob.texture;
  321. } else {
  322. texture = createTexture(
  323. this._gltf,
  324. this._textureInfo,
  325. this._image,
  326. this._mipLevels,
  327. frameState.context
  328. );
  329. }
  330. // Unload everything except the texture
  331. this.unload();
  332. this._texture = texture;
  333. this._state = ResourceLoaderState.READY;
  334. this._promise.resolve(this);
  335. };
  336. /**
  337. * Unloads the resource.
  338. * @private
  339. */
  340. GltfTextureLoader.prototype.unload = function () {
  341. if (defined(this._texture)) {
  342. this._texture.destroy();
  343. }
  344. if (defined(this._imageLoader)) {
  345. this._resourceCache.unload(this._imageLoader);
  346. }
  347. this._imageLoader = undefined;
  348. this._image = undefined;
  349. this._mipLevels = undefined;
  350. this._texture = undefined;
  351. this._gltf = undefined;
  352. };