UNPKG

@loaders.gl/textures

Version:

Framework-independent loaders for compressed and super compressed (basis) textures

443 lines 15.3 kB
// loaders.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { extractLoadLibraryOptions } from '@loaders.gl/worker-utils'; import { loadBasisEncoderModule, loadBasisTranscoderModule } from "./basis-module-loader.js"; import { GL_COMPRESSED_RED_GREEN_RGTC2_EXT, GL_COMPRESSED_RED_RGTC1_EXT, GL_COMPRESSED_RGB_ATC_WEBGL, GL_COMPRESSED_RGB_ETC1_WEBGL, GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG, GL_COMPRESSED_RGB_S3TC_DXT1_EXT, GL_COMPRESSED_RGBA8_ETC2_EAC, GL_COMPRESSED_RGBA_ASTC_4x4_KHR, GL_COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL, GL_COMPRESSED_RGBA_BPTC_UNORM_EXT, GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, GL_RGB565, GL_RGBA4, GL_RGBA8 } from "../gl-extensions.js"; import { detectSupportedTextureFormats } from "../utils/detect-supported-texture-formats.js"; import { isKTX } from "./parse-ktx.js"; let basisTranscodingLock = Promise.resolve(); export const BASIS_FORMAT_TO_OUTPUT_OPTIONS = { etc1: { basisFormat: 0, compressed: true, format: GL_COMPRESSED_RGB_ETC1_WEBGL, textureFormat: 'etc1-rgb-unorm-webgl' }, etc2: { basisFormat: 1, compressed: true, format: GL_COMPRESSED_RGBA8_ETC2_EAC, textureFormat: 'etc2-rgba8unorm' }, bc1: { basisFormat: 2, compressed: true, format: GL_COMPRESSED_RGB_S3TC_DXT1_EXT, textureFormat: 'bc1-rgb-unorm-webgl' }, bc3: { basisFormat: 3, compressed: true, format: GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, textureFormat: 'bc3-rgba-unorm' }, bc4: { basisFormat: 4, compressed: true, format: GL_COMPRESSED_RED_RGTC1_EXT, textureFormat: 'bc4-r-unorm' }, bc5: { basisFormat: 5, compressed: true, format: GL_COMPRESSED_RED_GREEN_RGTC2_EXT, textureFormat: 'bc5-rg-unorm' }, 'bc7-m6-opaque-only': { basisFormat: 6, compressed: true, format: GL_COMPRESSED_RGBA_BPTC_UNORM_EXT, textureFormat: 'bc7-rgba-unorm' }, 'bc7-m5': { basisFormat: 7, compressed: true, format: GL_COMPRESSED_RGBA_BPTC_UNORM_EXT, textureFormat: 'bc7-rgba-unorm' }, 'pvrtc1-4-rgb': { basisFormat: 8, compressed: true, format: GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG, textureFormat: 'pvrtc-rgb4unorm-webgl' }, 'pvrtc1-4-rgba': { basisFormat: 9, compressed: true, format: GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG, textureFormat: 'pvrtc-rgba4unorm-webgl' }, 'astc-4x4': { basisFormat: 10, compressed: true, format: GL_COMPRESSED_RGBA_ASTC_4x4_KHR, textureFormat: 'astc-4x4-unorm' }, 'atc-rgb': { basisFormat: 11, compressed: true, format: GL_COMPRESSED_RGB_ATC_WEBGL, textureFormat: 'atc-rgb-unorm-webgl' }, 'atc-rgba-interpolated-alpha': { basisFormat: 12, compressed: true, format: GL_COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL, textureFormat: 'atc-rgbai-unorm-webgl' }, rgba32: { basisFormat: 13, compressed: false, format: GL_RGBA8, textureFormat: 'rgba8unorm' }, rgb565: { basisFormat: 14, compressed: false, format: GL_RGB565, textureFormat: 'rgb565unorm-webgl' }, bgr565: { basisFormat: 15, compressed: false, format: GL_RGB565, textureFormat: 'rgb565unorm-webgl' }, rgba4444: { basisFormat: 16, compressed: false, format: GL_RGBA4, textureFormat: 'rgba4unorm-webgl' } }; export const BASIS_FORMATS = Object.freeze(Object.keys(BASIS_FORMAT_TO_OUTPUT_OPTIONS)); /** * Serializes access to the Basis transcoder so concurrent callers do not enter the non-reentrant * decoder path at the same time. * @param transcode - Transcode operation to run with exclusive access. * @returns The transcode result. */ export async function withBasisTranscodingLock(transcode) { const previousLock = basisTranscodingLock; let releaseLock; basisTranscodingLock = new Promise((resolve) => { releaseLock = resolve; }); await previousLock; try { return await transcode(); } finally { releaseLock(); } } /** * parse data with a Binomial Basis_Universal module * @param data * @param options * @returns compressed texture data */ // eslint-disable-next-line complexity export async function parseBasis(data, options = {}) { const loadLibraryOptions = extractLoadLibraryOptions(options); return await withBasisTranscodingLock(async () => { if (!options.basis?.containerFormat || options.basis.containerFormat === 'auto') { if (isKTX(data)) { const fileConstructors = await loadBasisEncoderModule(loadLibraryOptions); return parseKTX2File(fileConstructors.KTX2File, data, options); } const { BasisFile } = await loadBasisTranscoderModule(loadLibraryOptions); return parseBasisFile(BasisFile, data, options); } switch (options.basis.module) { case 'encoder': const fileConstructors = await loadBasisEncoderModule(loadLibraryOptions); switch (options.basis.containerFormat) { case 'ktx2': return parseKTX2File(fileConstructors.KTX2File, data, options); case 'basis': default: return parseBasisFile(fileConstructors.BasisFile, data, options); } case 'transcoder': default: const { BasisFile } = await loadBasisTranscoderModule(loadLibraryOptions); return parseBasisFile(BasisFile, data, options); } }); } /** * Parse *.basis file data * @param BasisFile - initialized transcoder module * @param data * @param options * @returns compressed texture data */ function parseBasisFile(BasisFile, data, options) { const basisFile = new BasisFile(new Uint8Array(data)); try { if (!basisFile.startTranscoding()) { throw new Error('Failed to start basis transcoding'); } const imageCount = basisFile.getNumImages(); const images = []; for (let imageIndex = 0; imageIndex < imageCount; imageIndex++) { const levelsCount = basisFile.getNumLevels(imageIndex); const levels = []; for (let levelIndex = 0; levelIndex < levelsCount; levelIndex++) { levels.push(transcodeImage(basisFile, imageIndex, levelIndex, options)); } images.push(levels); } return images; } finally { basisFile.close(); basisFile.delete(); } } /** * Parse the particular level image of a basis file * @param basisFile * @param imageIndex * @param levelIndex * @param options * @returns compressed texture data */ function transcodeImage(basisFile, imageIndex, levelIndex, options) { const width = basisFile.getImageWidth(imageIndex, levelIndex); const height = basisFile.getImageHeight(imageIndex, levelIndex); // See https://github.com/BinomialLLC/basis_universal/pull/83 const hasAlpha = basisFile.getHasAlpha( /* imageIndex, levelIndex */); // Check options for output format etc const { compressed, format, basisFormat, textureFormat } = getBasisOptions(options, hasAlpha); const decodedSize = basisFile.getImageTranscodedSizeInBytes(imageIndex, levelIndex, basisFormat); const decodedData = new Uint8Array(decodedSize); if (!basisFile.transcodeImage(decodedData, imageIndex, levelIndex, basisFormat, 0, 0)) { throw new Error('failed to start Basis transcoding'); } return { // standard loaders.gl image category payload shape: 'texture-level', width, height, data: decodedData, compressed, ...(format !== undefined ? { format } : {}), ...(textureFormat !== undefined ? { textureFormat } : {}), // Additional fields // Add levelSize field. hasAlpha }; } /** * Parse *.ktx2 file data * @param KTX2File * @param data * @param options * @returns compressed texture data */ function parseKTX2File(KTX2File, data, options) { const ktx2File = new KTX2File(new Uint8Array(data)); try { if (!ktx2File.startTranscoding()) { throw new Error('failed to start KTX2 transcoding'); } const levelsCount = ktx2File.getLevels(); const levels = []; for (let levelIndex = 0; levelIndex < levelsCount; levelIndex++) { levels.push(transcodeKTX2Image(ktx2File, levelIndex, options)); } return [levels]; } finally { ktx2File.close(); ktx2File.delete(); } } /** * Parse the particular level image of a ktx2 file * @param ktx2File * @param levelIndex * @param options * @returns */ function transcodeKTX2Image(ktx2File, levelIndex, options) { const { alphaFlag, height, width } = ktx2File.getImageLevelInfo(levelIndex, 0, 0); // Check options for output format etc const { compressed, format, basisFormat, textureFormat } = getBasisOptions(options, alphaFlag); const decodedSize = ktx2File.getImageTranscodedSizeInBytes(levelIndex, 0 /* layerIndex */, 0 /* faceIndex */, basisFormat); const decodedData = new Uint8Array(decodedSize); if (!ktx2File.transcodeImage(decodedData, levelIndex, 0 /* layerIndex */, 0 /* faceIndex */, basisFormat, 0, -1 /* channel0 */, -1 /* channel1 */)) { throw new Error('Failed to transcode KTX2 image'); } return { // standard loaders.gl image category payload shape: 'texture-level', width, height, data: decodedData, compressed, ...(format !== undefined ? { format } : {}), ...(textureFormat !== undefined ? { textureFormat } : {}), // Additional fields levelSize: decodedSize, hasAlpha: alphaFlag }; } /** * Get BasisFormat by loader format option * @param options * @param hasAlpha * @returns BasisFormat data */ function getBasisOptions(options, hasAlpha) { let format = options.basis?.format || 'auto'; if (format === 'auto') { format = options.basis?.supportedTextureFormats ? selectSupportedBasisFormat(options.basis.supportedTextureFormats) : selectSupportedBasisFormat(); } if (typeof format === 'object') { format = hasAlpha ? format.alpha : format.noAlpha; } const normalizedFormat = format.toLowerCase(); const basisOutputOptions = BASIS_FORMAT_TO_OUTPUT_OPTIONS[normalizedFormat]; if (!basisOutputOptions) { throw new Error(`Unknown Basis format ${format}`); } return basisOutputOptions; } export function selectSupportedBasisFormat(supportedTextureFormats = detectSupportedTextureFormats()) { const textureFormats = new Set(supportedTextureFormats); if (hasSupportedTextureFormat(textureFormats, ['astc-4x4-unorm', 'astc-4x4-unorm-srgb'])) { return 'astc-4x4'; } else if (hasSupportedTextureFormat(textureFormats, ['bc7-rgba-unorm', 'bc7-rgba-unorm-srgb'])) { return { alpha: 'bc7-m5', noAlpha: 'bc7-m6-opaque-only' }; } else if (hasSupportedTextureFormat(textureFormats, [ 'bc1-rgb-unorm-webgl', 'bc1-rgb-unorm-srgb-webgl', 'bc1-rgba-unorm', 'bc1-rgba-unorm-srgb', 'bc2-rgba-unorm', 'bc2-rgba-unorm-srgb', 'bc3-rgba-unorm', 'bc3-rgba-unorm-srgb' ])) { return { alpha: 'bc3', noAlpha: 'bc1' }; } else if (hasSupportedTextureFormat(textureFormats, [ 'pvrtc-rgb4unorm-webgl', 'pvrtc-rgba4unorm-webgl', 'pvrtc-rgb2unorm-webgl', 'pvrtc-rgba2unorm-webgl' ])) { return { alpha: 'pvrtc1-4-rgba', noAlpha: 'pvrtc1-4-rgb' }; } else if (hasSupportedTextureFormat(textureFormats, [ 'etc2-rgb8unorm', 'etc2-rgb8unorm-srgb', 'etc2-rgb8a1unorm', 'etc2-rgb8a1unorm-srgb', 'etc2-rgba8unorm', 'etc2-rgba8unorm-srgb', 'eac-r11unorm', 'eac-r11snorm', 'eac-rg11unorm', 'eac-rg11snorm' ])) { return 'etc2'; } else if (textureFormats.has('etc1-rgb-unorm-webgl')) { return 'etc1'; } else if (hasSupportedTextureFormat(textureFormats, [ 'atc-rgb-unorm-webgl', 'atc-rgba-unorm-webgl', 'atc-rgbai-unorm-webgl' ])) { return { alpha: 'atc-rgba-interpolated-alpha', noAlpha: 'atc-rgb' }; } return 'rgb565'; } export function getSupportedBasisFormats(supportedTextureFormats = detectSupportedTextureFormats()) { const textureFormats = new Set(supportedTextureFormats); const basisFormats = []; if (hasSupportedTextureFormat(textureFormats, ['astc-4x4-unorm', 'astc-4x4-unorm-srgb'])) { basisFormats.push('astc-4x4'); } if (hasSupportedTextureFormat(textureFormats, [ 'bc1-rgb-unorm-webgl', 'bc1-rgb-unorm-srgb-webgl', 'bc1-rgba-unorm', 'bc1-rgba-unorm-srgb', 'bc2-rgba-unorm', 'bc2-rgba-unorm-srgb', 'bc3-rgba-unorm', 'bc3-rgba-unorm-srgb' ])) { basisFormats.push('bc1', 'bc3'); } if (hasSupportedTextureFormat(textureFormats, ['bc4-r-unorm', 'bc4-r-snorm'])) { basisFormats.push('bc4'); } if (hasSupportedTextureFormat(textureFormats, ['bc5-rg-unorm', 'bc5-rg-snorm'])) { basisFormats.push('bc5'); } if (hasSupportedTextureFormat(textureFormats, ['bc7-rgba-unorm', 'bc7-rgba-unorm-srgb'])) { basisFormats.push('bc7-m5', 'bc7-m6-opaque-only'); } if (hasSupportedTextureFormat(textureFormats, [ 'pvrtc-rgb4unorm-webgl', 'pvrtc-rgba4unorm-webgl', 'pvrtc-rgb2unorm-webgl', 'pvrtc-rgba2unorm-webgl' ])) { basisFormats.push('pvrtc1-4-rgb', 'pvrtc1-4-rgba'); } if (hasSupportedTextureFormat(textureFormats, [ 'etc2-rgb8unorm', 'etc2-rgb8unorm-srgb', 'etc2-rgb8a1unorm', 'etc2-rgb8a1unorm-srgb', 'etc2-rgba8unorm', 'etc2-rgba8unorm-srgb', 'eac-r11unorm', 'eac-r11snorm', 'eac-rg11unorm', 'eac-rg11snorm' ])) { basisFormats.push('etc2'); } if (textureFormats.has('etc1-rgb-unorm-webgl')) { basisFormats.push('etc1'); } if (hasSupportedTextureFormat(textureFormats, [ 'atc-rgb-unorm-webgl', 'atc-rgba-unorm-webgl', 'atc-rgbai-unorm-webgl' ])) { basisFormats.push('atc-rgb', 'atc-rgba-interpolated-alpha'); } basisFormats.push('rgba32', 'rgb565', 'bgr565', 'rgba4444'); return basisFormats; } function hasSupportedTextureFormat(supportedTextureFormats, candidateTextureFormats) { return candidateTextureFormats.some((textureFormat) => supportedTextureFormats.has(textureFormat)); } //# sourceMappingURL=parse-basis.js.map