import { Layer } from './Layer.js' import { Raster } from './Raster.js' import { ShaderRTI } from './ShaderRTI.js' import { Transform } from './Transform.js' /** * @typedef {Object} LayerRTIOptions * @property {string} url - URL to RTI info.json file (required) * @property {string} layout - Layout type: 'image', 'deepzoom', 'google', 'iiif', 'zoomify', 'tarzoom', 'itarzoom' * @property {boolean} [normals=false] - Whether to load normal maps * @property {string} [server] - IIP server URL (for IIP layout) * @property {number} [worldRotation=0] - Global rotation offset * @extends LayerOptions */ /** * LayerRTI implements Reflectance Transformation Imaging (RTI) visualization. * * RTI is an imaging technique that captures surface reflectance data to enable * interactive relighting of an object from different directions. The layer handles * the 'relight' data format, which consists of: * * - info.json: Contains RTI parameters and configuration * - plane_*.jpg: Series of coefficient images * - normals.jpg: Optional normal map (when using normals=true) * * Features: * - Interactive relighting * - Multiple layout support * - Normal map integration * - Light direction control * - Animation support * - World rotation handling * * Technical Details: * - Uses coefficient-based relighting * - Supports multiple image planes * - Handles various tiling schemes * - Manages WebGL resources * - Coordinates light transformations * * Data Format Support: * - Relight JSON configuration * - Multiple layout systems * - JPEG coefficient planes * - Optional normal maps * - IIP image protocol * * @extends Layer * * @example * ```javascript * // Create RTI layer with deepzoom layout * const rtiLayer = new OpenLIME.Layer({ * type: 'rti', * url: 'path/to/info.json', * layout: 'deepzoom', * normals: true * }); * * // Add to viewer * viewer.addLayer('rti', rtiLayer); * * // Change light direction with animation * rtiLayer.setLight([0.5, 0.5], 1000); * ``` * * @see {@link https://github.com/cnr-isti-vclab/relight|Relight on GitHub} */ class LayerRTI extends Layer { /** * Creates a new LayerRTI instance * @param {LayerRTIOptions} options - Configuration options * @throws {Error} If rasters options is not empty * @throws {Error} If url is not provided */ constructor(options) { super(options); if (Object.keys(this.rasters).length != 0) throw "Rasters options should be empty!"; if (!this.url) throw "Url option is required"; this.shaders['rti'] = new ShaderRTI({ normals: this.normals }); this.setShader('rti'); this.addControl('light', [0, 0]); this.worldRotation = 0; //if the canvas or ethe layer rotate, light direction neeeds to be rotated too. this.loadJson(this.url); } /** * Constructs URL for image plane resources based on layout type * @param {string} url - Base URL * @param {string} plane - Plane identifier * @returns {string} Complete URL for the resource * @private */ imageUrl(url, plane) { let path = this.url.substring(0, this.url.lastIndexOf('/') + 1); switch (this.layout.type) { case 'image': return path + plane + '.jpg'; break; case 'google': return path + plane; break; case 'deepzoom': return path + plane + '.dzi'; break; case 'tarzoom': return path + plane + '.tzi'; break; case 'itarzoom': return path + 'planes.tzi'; break; case 'zoomify': return path + plane + '/ImageProperties.xml'; break; case 'iip': return url; break; case 'iiif': throw Error("Unimplemented"); default: throw Error("Unknown layout: " + layout.type); } } /** * Sets the light direction with optional animation * @param {number[]} light - Light direction vector [x, y] * @param {number} [dt] - Animation duration in milliseconds */ setLight(light, dt) { this.setControl('light', light, dt); } /** * Loads and processes RTI configuration * @param {string} url - URL to info.json * @private * @async */ loadJson(url) { (async () => { let infoUrl = url; // Need to handle embedded RTI info.json when using IIP and TIFF image stacks if (this.layout.type == "iip") infoUrl = (this.server ? this.server + '?FIF=' : '') + url + "&obj=description"; var response = await fetch(infoUrl); if (!response.ok) { this.status = "Failed loading " + infoUrl + ": " + response.statusText; return; } let json = await response.json(); // Update layout image format and pixelSize if provided in info.json this.layout.suffix = json.format; if (json.pixelSizeInMM) this.pixelSize = json.pixelSizeInMM; this.shader.init(json); let urls = []; for (let p = 0; p < this.shader.njpegs; p++) { let imageUrl = this.layout.imageUrl(url, 'plane_' + p); urls.push(imageUrl); let raster = new Raster({ format: 'vec3' }); this.rasters.push(raster); } if (this.normals) { // ITARZOOM must include normals and currently has a limitation: loads the entire tile let imageUrl = this.layout.imageUrl(url, 'normals'); urls.push(imageUrl); let raster = new Raster({ format: 'vec3' }); this.rasters.push(raster); } this.layout.setUrls(urls); })().catch(e => { console.log(e); this.status = e; }); } /** * Updates light direction based on control state * Handles world rotation transformations * @returns {boolean} Whether interpolation is complete * @override * @private */ interpolateControls() { let done = super.interpolateControls(); if (!done) { let light = this.controls['light'].current.value; //this.shader.setLight(light); let rotated = Transform.rotate(light[0], light[1], this.worldRotation * Math.PI); this.shader.setLight([rotated.x, rotated.y]); } return done; } /** * Renders the RTI visualization * Updates world rotation and manages drawing * @param {Transform} transform - Current view transform * @param {Object} viewport - Current viewport * @returns {boolean} Whether render completed successfully * @override * @private */ draw(transform, viewport) { this.worldRotation = transform.a + this.transform.a; return super.draw(transform, viewport); } } /** * Register this layer type with the Layer factory * @type {Function} * @private */ Layer.prototype.types['rti'] = (options) => { return new LayerRTI(options); } export { LayerRTI }