import { Raster } from './Raster.js'; /** * @typedef {('r16f'|'rg16f'|'rgb16f'|'rgba16f'|'r16ui'|'rg16ui'|'rgb16ui'|'rgba16ui'|'r16i'|'rg16i'|'rgb16i'|'rgba16i'|'depth16')} Raster16Bit#Format * Defines the 16-bit format for image data storage in textures. * @property {'r16f'} r16f - Single-channel 16-bit floating point format * @property {'rg16f'} rg16f - Two-channel 16-bit floating point format * @property {'rgb16f'} rgb16f - Three-channel 16-bit floating point format * @property {'rgba16f'} rgba16f - Four-channel 16-bit floating point format * @property {'r16ui'} r16ui - Single-channel 16-bit unsigned integer format * @property {'rg16ui'} rg16ui - Two-channel 16-bit unsigned integer format * @property {'rgb16ui'} rgb16ui - Three-channel 16-bit unsigned integer format * @property {'rgba16ui'} rgba16ui - Four-channel 16-bit unsigned integer format * @property {'r16i'} r16i - Single-channel 16-bit signed integer format * @property {'rg16i'} rg16i - Two-channel 16-bit signed integer format * @property {'rgb16i'} rgb16i - Three-channel 16-bit signed integer format * @property {'rgba16i'} rgba16i - Four-channel 16-bit signed integer format * @property {'depth16'} depth16 - 16-bit depth texture format */ /** * @typedef {Function} DataLoaderCallback * @param {Object} tile - The tile information object * @param {WebGL2RenderingContext} gl - The WebGL2 rendering context * @param {Object} options - Additional options for the data loader * @returns {Promise<Object>} The loaded data object with properties: * - data: TypedArray or Image data * - width: Width of the image * - height: Height of the image * - channels: Number of channels in the data */ /** * Raster16Bit class extends Raster to handle 16-bit textures with WebGL 2.0. * Provides functionality for: * - Loading 16-bit images from URLs or blobs via custom data loaders * - Converting data to appropriate WebGL 2.0 texture formats * - Supporting various 16-bit formats (float, int, uint) * - Creating appropriate texture parameters for 16-bit data * - Support for custom data loaders for specialized formats */ class Raster16Bit extends Raster { /** * Creates a new Raster16Bit instance. * @param {Object} [options] - Configuration options * @param {Raster16Bit#Format} [options.format='rgb16ui'] - 16-bit data format * @param {boolean} [options.useHalfFloat=false] - Use HALF_FLOAT type instead of FLOAT for better performance when applicable * @param {boolean} [options.flipY=false] - Whether to flip the image vertically during loading * @param {boolean} [options.premultiplyAlpha=false] - Whether to premultiply alpha during loading * @param {DataLoaderCallback} [options.dataLoader=null] - Custom data loader callback * @param {Object} [options.dataLoaderOptions={}] - Options to pass to the data loader * @param {boolean} [options.debug=false] - Enable debug output */ constructor(options) { // Initialize with parent constructor but override defaults super(Object.assign({ format: 'rgb16ui', debug: false, useHalfFloat: false, flipY: false, premultiplyAlpha: false, }, options)); // Additional options specific to 16-bit handling Object.assign(this, { dataLoader: null, dataLoaderOptions: {}, statInfo: {} }); // Override with provided options if (options) { Object.assign(this, options); } // Check if the format is supported if (!this._isFormatSupported(this.format)) { throw new Error(`The format "${this.format}" is not supported by the browser.`); } if (this.debug) { console.log(`Raster16Bit created with format: ${this.format}`); } } /** * Gets the number of components for the current format * @private * @returns {number} Number of components (1, 2, 3, or 4) */ _getComponentCount() { if (this.format.startsWith('r16') && !this.format.startsWith('rg16') && !this.format.startsWith('rgb16') && !this.format.startsWith('rgba16')) { return 1; // Single channel (r16f, r16ui, r16i) } else if (this.format.startsWith('rg16')) { return 2; // Two channels (rg16f, rg16ui, rg16i) } else if (this.format.startsWith('rgb16')) { return 3; // Three channels (rgb16f, rgb16ui, rgb16i) } else if (this.format.startsWith('rgba16')) { return 4; // Four channels (rgba16f, rgba16ui, rgba16i) } else if (this.format === 'depth16') { return 1; // Depth is single channel } return 1; // Default to 1 if unknown } /** * Loads a 16-bit image tile and converts it to a WebGL texture. * Overrides parent method to handle 16-bit specific formats. * @async * @param {Object} tile - The tile to load * @param {string} tile.url - URL of the image * @param {number} [tile.start] - Start byte for partial requests * @param {number} [tile.end] - End byte for partial requests * @param {WebGL2RenderingContext} gl - The WebGL2 rendering context * @returns {Promise<Array>} Promise resolving to [texture, size]: * - texture: WebGLTexture object * - size: Size of the image in bytes (width * height * components * bytesPerComponent) * @throws {Error} If context is not WebGL2 */ async loadImage(tile, gl) { // Ensure we have a WebGL2 context if (!(gl instanceof WebGL2RenderingContext)) { throw new Error("WebGL2 context is required for 16-bit textures"); } if (this.debug) { console.log(`Raster16Bit.loadImage called for URL: ${tile.url}`); } let imageData; // Use the appropriate data loader if (this.dataLoader) { // Use custom data loader if provided if (this.debug) { console.log("Using custom data loader"); } try { imageData = await this.dataLoader(tile, gl, this.dataLoaderOptions); this.statInfo.maxValue = imageData.statistics.maxValue; this.statInfo.avgLuminance = imageData.statistics.avgLuminance; this.statInfo.percentileLuminance = imageData.statistics.percentileLuminance; this.emit('loaded'); if (this.debug) { console.log(`Data loader returned: ${imageData.width}x${imageData.height}, ${imageData.channels} channels`); } } catch (error) { console.error("Error in data loader:", error); throw error; } } else { // Use default parent class loading mechanism if no dataLoader provided if (this.debug) { console.log("Using default loader mechanism"); } try { let [tex, size] = await super.loadImage(tile, gl); // Adjust size calculation for 16-bit (2 bytes per component) size = this.width * this.height * this._getComponentCount() * 2; return [tex, size]; } catch (error) { console.error("Error in default loader:", error); throw error; } } // Store dimensions this.width = imageData.width; this.height = imageData.height; if (this.debug) { console.log(`Creating texture: ${this.width}x${this.height}`); } // Create texture from the loaded data const tex = this._createTextureFromData(gl, imageData.data, imageData.width, imageData.height, imageData.channels); // Calculate size in bytes const bytesPerComponent = 2; // 16 bits = 2 bytes const size = imageData.width * imageData.height * imageData.channels * bytesPerComponent; return [tex, size]; } getStatInfo() { return this.statInfo; } /** * Creates a WebGL2 texture from raw data. * @private * @param {WebGL2RenderingContext} gl - The WebGL2 rendering context * @param {TypedArray} data - The raw pixel data * @param {number} width - Width of the image * @param {number} height - Height of the image * @param {number} channels - Number of channels in the data * @returns {WebGLTexture} The created texture */ _createTextureFromData(gl, data, width, height, channels) { if (this.debug) { console.log(`Creating texture from data: ${width}x${height}, ${channels} channels, data type: ${data.constructor.name}`); } const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); // Set texture parameters gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this.flipY); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha); gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); // Determine format parameters based on format const formatParams = this._getFormatParameters(gl, channels); if (this.debug) { console.log("Format parameters:", formatParams); } try { // Upload data to texture gl.texImage2D( gl.TEXTURE_2D, // target 0, // level formatParams.internalFormat, // internalformat width, // width height, // height 0, // border formatParams.format, // format formatParams.type, // type data // pixels ); } catch (error) { console.error("Error creating texture:", error); throw error; } // Set filtering and wrapping parameters if (width > 1024 || height > 1024) { gl.generateMipmap(gl.TEXTURE_2D); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); } else { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); } gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // Store color space information on the texture this._texture = tex; return tex; } /** * Get format parameters for WebGL texture creation based on format and channels. * @private * @param {WebGL2RenderingContext} gl - The WebGL2 rendering context * @param {number} channels - Number of channels in the data * @returns {Object} Object with internalFormat, format, and type properties */ _getFormatParameters(gl, channels) { let internalFormat, format, type; // Determine format parameters based on the specified format if (this.format.includes('16f')) { // Floating point formats type = this.useHalfFloat ? gl.HALF_FLOAT : gl.FLOAT; switch (channels) { case 1: internalFormat = gl.R16F; format = gl.RED; break; case 2: internalFormat = gl.RG16F; format = gl.RG; break; case 3: internalFormat = gl.RGB16F; format = gl.RGB; break; case 4: internalFormat = gl.RGBA16F; format = gl.RGBA; break; default: throw new Error(`Unsupported channel count: ${channels}`); } } else if (this.format.includes('16ui')) { // Unsigned integer formats type = gl.UNSIGNED_SHORT; switch (channels) { case 1: internalFormat = gl.R16UI; format = gl.RED_INTEGER; break; case 2: internalFormat = gl.RG16UI; format = gl.RG_INTEGER; break; case 3: internalFormat = gl.RGB16UI; format = gl.RGB_INTEGER; break; case 4: internalFormat = gl.RGBA16UI; format = gl.RGBA_INTEGER; break; default: throw new Error(`Unsupported channel count: ${channels}`); } } else if (this.format.includes('16i')) { // Signed integer formats type = gl.SHORT; switch (channels) { case 1: internalFormat = gl.R16I; format = gl.RED_INTEGER; break; case 2: internalFormat = gl.RG16I; format = gl.RG_INTEGER; break; case 3: internalFormat = gl.RGB16I; format = gl.RGB_INTEGER; break; case 4: internalFormat = gl.RGBA16I; format = gl.RGBA_INTEGER; break; default: throw new Error(`Unsupported channel count: ${channels}`); } } else if (this.format === 'depth16') { // Depth texture internalFormat = gl.DEPTH_COMPONENT16; format = gl.DEPTH_COMPONENT; type = gl.UNSIGNED_SHORT; } else { throw new Error(`Unsupported format: ${this.format}`); } return { internalFormat, format, type }; } /** * Checks if the specified format is supported by the browser. * Also verifies that required WebGL extensions are available. * @private * @param {string} format - The format to check * @returns {boolean} True if the format is supported, false otherwise */ _isFormatSupported(format) { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl2'); if (!gl) { console.error('WebGL2 is not supported by this browser.'); return false; } const formatMap = { 'r16f': { internalFormat: gl.R16F, requiredExtensions: ['EXT_color_buffer_float'] }, 'rg16f': { internalFormat: gl.RG16F, requiredExtensions: ['EXT_color_buffer_float'] }, 'rgb16f': { internalFormat: gl.RGB16F, requiredExtensions: ['EXT_color_buffer_float'] }, 'rgba16f': { internalFormat: gl.RGBA16F, requiredExtensions: ['EXT_color_buffer_float'] }, 'r16ui': { internalFormat: gl.R16UI, requiredExtensions: [] }, 'rg16ui': { internalFormat: gl.RG16UI, requiredExtensions: [] }, 'rgb16ui': { internalFormat: gl.RGB16UI, requiredExtensions: [] }, 'rgba16ui': { internalFormat: gl.RGBA16UI, requiredExtensions: [] }, 'r16i': { internalFormat: gl.R16I, requiredExtensions: [] }, 'rg16i': { internalFormat: gl.RG16I, requiredExtensions: [] }, 'rgb16i': { internalFormat: gl.RGB16I, requiredExtensions: [] }, 'rgba16i': { internalFormat: gl.RGBA16I, requiredExtensions: [] }, 'depth16': { internalFormat: gl.DEPTH_COMPONENT16, requiredExtensions: [] } }; const formatInfo = formatMap[format]; if (!formatInfo) { console.error(`Unknown format: ${format}`); return false; } // Check for required extensions for (const extension of formatInfo.requiredExtensions) { if (!gl.getExtension(extension)) { console.error(`Required WebGL extension "${extension}" is not supported for format "${format}".`); return false; } } // Check if the internal format is supported const isSupported = gl.getInternalformatParameter(gl.RENDERBUFFER, formatInfo.internalFormat, gl.SAMPLES); return isSupported && isSupported.length > 0; } } export { Raster16Bit };