import { Shader } from './Shader.js' /** * @typedef {Object} ShaderRTI~Basis * Configuration data for basis functions * @property {Float32Array} [basis] - PCA basis for rbf and bln modes * @property {number[][]} [lights] - Light directions for rbf interpolation * @property {number} [sigma] - RBF interpolation parameter * @property {number} [ndimensions] - PCA dimension space */ /** * @typedef {Object} ShaderRTI~Options * Configuration options for RTI shader * @property {string} [mode='normal'] - Initial rendering mode * @property {string} [type] - Basis type: 'ptm'|'hsh'|'sh'|'rbf'|'bln' * @property {string} [colorspace] - Color space: 'lrgb'|'rgb'|'mrgb'|'mycc' * @property {number} [nplanes] - Number of coefficient planes * @property {number[]} [yccplanes] - Number of planes for YCC components * @property {Object} [material] - Material parameters for dequantization */ /** * ShaderRTI implements various Reflectance Transformation Imaging techniques. * Works in conjunction with LayerRTI for interactive relighting of cultural heritage objects. * * Supported Basis Types: * - PTM (Polynomial Texture Maps) * - HSH (Hemispherical Harmonics) * - SH (Spherical Harmonics) * - RBF (Radial Basis Functions) * - BLN (Bilinear Interpolation) * * Features: * - Multiple rendering modes (light, normals, diffuse, specular) * - Various color space support * - Automatic basis selection * - Real-time coefficient interpolation * - Normal map visualization * - Material property control * * Technical Implementation: * - Efficient GPU-based relighting * - Dynamic shader generation * - Coefficient plane management * - Light vector transformation * - Color space conversion * * @extends Shader */ class ShaderRTI extends Shader { /** * Creates a new RTI shader * @param {ShaderRTI~Options} [options] - Configuration options * * @example * ```javascript * // Create PTM shader * const shader = new ShaderRTI({ * type: 'ptm', * colorspace: 'rgb', * mode: 'light' * }); * ``` */ constructor(options) { super({}); Object.assign(this, { modes: ['light', 'normals', 'diffuse', 'specular'], mode: 'normal', type: ['ptm', 'hsh', 'sh', 'rbf', 'bln'], colorspaces: ['lrgb', 'rgb', 'mrgb', 'mycc'], nplanes: null, //number of coefficient planes yccplanes: null, //number of luminance planes for mycc color space njpegs: null, //number of textures needed (ceil(nplanes/3)) material: null, //material parameters lights: null, //light directions (needed for rbf interpolation) sigma: null, //rbf interpolation parameter ndimensions: null, //PCA dimension space (for rbf and bln) scale: null, //factor and bias are used to dequantize coefficient planes. bias: null, basis: null, //PCA basis for rbf and bln lweights: null //light direction dependent coefficients to be used with coefficient planes }); Object.assign(this, options); if (this.relight) this.init(this.relight); this.setMode('light'); } /** * Sets the rendering mode * @param {string} mode - One of: 'light', 'normals', 'diffuse', 'specular' * @throws {Error} If mode is not recognized */ setMode(mode) { if (!(this.modes.includes(mode))) throw Error("Unknown mode: " + mode); this.mode = mode; if (mode != 'light') { this.lightWeights([0.612, 0.354, 0.707], 'base'); this.lightWeights([-0.612, 0.354, 0.707], 'base1'); this.lightWeights([0, -0.707, 0.707], 'base2'); } this.needsUpdate = true; } /** * Updates light direction for relighting * @param {number[]} light - Light vector [x, y], automatically normalized * @throws {Error} If shader is not initialized */ setLight(light) { if (!this.uniforms.light) throw "Shader not initialized, wait on layer ready event for setLight." let x = light[0]; let y = light[1]; //map the square to the circle. let r = Math.sqrt(x * x + y * y); if (r > 1) { x /= r; y /= r; } let z = Math.sqrt(Math.max(0, 1 - x * x - y * y)); light = [x, y, z]; if (this.mode == 'light') this.lightWeights(light, 'base'); this.setUniform('light', light); } /** * Sets specular exponent for specular enhancement mode * @param {number} value - Specular exponent */ setSpecularExp(value) { this.setUniform('specular_exp', value); } /** * Initializes shader with RTI configuration * @param {Object} relight - RTI configuration data * @param {string} relight.type - Basis type * @param {string} relight.colorspace - Color space * @param {Object} relight.material - Material parameters * @param {number[]} relight.basis - Optional PCA basis */ init(relight) { Object.assign(this, relight); if (this.colorspace == 'mycc') this.nplanes = this.yccplanes[0] + this.yccplanes[1] + this.yccplanes[2]; else this.yccplanes = [0, 0, 0]; this.planes = []; this.njpegs = 0; while (this.njpegs * 3 < this.nplanes) this.njpegs++; for (let i = 0; i < this.njpegs; i++) this.samplers.push({ id: i, name: 'plane' + i, type: 'vec3' }); if (this.normals) this.samplers.push({ id: this.njpegs, name: 'normals', type: 'vec3' }); this.material = this.materials[0]; if (this.lights) this.lights + new Float32Array(this.lights); if (this.type == "rbf") this.ndimensions = this.lights.length / 3; if (this.type == "bilinear") { this.ndimensions = this.resolution * this.resolution; this.type = "bln"; } this.scale = this.material.scale; this.bias = this.material.bias; if (['mrgb', 'mycc'].includes(this.colorspace)) this.loadBasis(this.basis); this.uniforms = { light: { type: 'vec3', needsUpdate: true, size: 3, value: [0.0, 0.0, 1] }, specular_exp: { type: 'float', needsUpdate: false, size: 1, value: 10 }, bias: { type: 'vec3', needsUpdate: true, size: this.nplanes / 3, value: this.bias }, scale: { type: 'vec3', needsUpdate: true, size: this.nplanes / 3, value: this.scale }, base: { type: 'vec3', needsUpdate: true, size: this.nplanes }, base1: { type: 'vec3', needsUpdate: false, size: this.nplanes }, base2: { type: 'vec3', needsUpdate: false, size: this.nplanes } } this.lightWeights([0, 0, 1], 'base'); } /** * Computes basis-specific light weights * @param {number[]} light - Light direction vector * @param {string} basename - Uniform name for weights * @param {number} [time] - Animation time * @private */ lightWeights(light, basename, time) { let value; switch (this.type) { case 'ptm': value = PTM.lightWeights(light); break; case 'hsh': value = HSH.lightWeights(light); break; case 'sh': value = SH.lightWeights(light); break; case 'rbf': value = RBF.lightWeights(light, this); break; case 'bln': value = BLN.lightWeights(light, this); break; } this.setUniform(basename, value, time); } baseLightOffset(p, l, k) { return (p * this.ndimensions + l) * 3 + k; } basePixelOffset(p, x, y, k) { return (p * this.resolution * this.resolution + (x + y * this.resolution)) * 3 + k; } loadBasis(data) { let tmp = new Uint8Array(data); this.basis = new Float32Array(data.length); let basis = new Float32Array(tmp.length); for (let plane = 0; plane < this.nplanes + 1; plane++) { for (let c = 0; c < this.ndimensions; c++) { for (let k = 0; k < 3; k++) { let o = this.baseLightOffset(plane, c, k); if (plane == 0) this.basis[o] = tmp[o] / 255; else this.basis[o] = ((tmp[o] - 127) / this.material.range[plane - 1]); } } } } fragShaderSrc(gl) { let basetype = 'vec3'; //(this.colorspace == 'mrgb' || this.colorspace == 'mycc')?'vec3':'float'; let gl2 = !(gl instanceof WebGLRenderingContext); let str = ` #define np1 ${this.nplanes + 1} ${gl2 ? 'in' : 'varying'} vec2 v_texcoord; const mat3 T = mat3(8.1650e-01, 4.7140e-01, 4.7140e-01, -8.1650e-01, 4.7140e-01, 4.7140e-01, -1.6222e-08, -9.4281e-01, 4.7140e-01); uniform vec3 light; uniform float specular_exp; uniform vec3 bias[np1]; uniform vec3 scale[np1]; uniform ${basetype} base[np1]; uniform ${basetype} base1[np1]; uniform ${basetype} base2[np1]; `; for (let n = 0; n < this.njpegs; n++) str += ` uniform sampler2D plane${n}; `; if (this.normals) str += ` uniform sampler2D normals; `; if (this.colorspace == 'mycc') str += ` const int ny0 = ${this.yccplanes[0]}; const int ny1 = ${this.yccplanes[1]}; ` switch (this.colorspace) { case 'lrgb': str += LRGB.render(this.njpegs, gl2); break; case 'rgb': str += RGB.render(this.njpegs, gl2); break; case 'mrgb': str += MRGB.render(this.njpegs, gl2); break; case 'mycc': str += MYCC.render(this.njpegs, this.yccplanes[0], gl2); break; } str += ` vec4 data() { `; if (this.mode == 'light') { str += ` vec4 color = render(base); `; } else { str += ` vec4 color; `; if (this.normals) str += ` vec3 normal = (texture${gl2 ? '' : '2D'}(normals, v_texcoord).zyx *2.0) - 1.0; normal.z = sqrt(1.0 - normal.x*normal.x - normal.y*normal.y); `; else str += ` vec3 normal; normal.x = dot(render(base ).xyz, vec3(1)); normal.y = dot(render(base1).xyz, vec3(1)); normal.z = dot(render(base2).xyz, vec3(1)); normal = normalize(T * normal); `; switch (this.mode) { case 'normals': str += ` normal = (normal + 1.0)/2.0; color = vec4(0.0, normal.xy, 1); `; break; case 'diffuse': if (this.colorspace == 'lrgb' || this.colorspace == 'rgb') str += ` vec4 diffuse = texture${gl2 ? '' : '2D'}(plane0, v_texcoord); float s = dot(light, normal); color = vec4(s * diffuse.xyz, 1); `; else str += ` color = vec4(vec3(dot(light, normal)), 1); `; break; case 'specular': default: str += ` float s = pow(dot(light, normal), specular_exp); //color = vec4(render(base).xyz*s, 1.0); color = vec4(s, s, s, 1.0); `; break; } } str += `return color; }`; return str; } } class LRGB { static render(njpegs, gl2) { let str = ` vec4 render(vec3 base[np1]) { float l = 0.0; ` for (let j = 1, k = 0; j < njpegs; j++, k += 3) { str += ` { vec4 c = texture${gl2 ? '' : '2D'}(plane${j}, v_texcoord); l += base[${k}].x*(c.x - bias[${j}].x)*scale[${j}].x; l += base[${k + 1}].x*(c.y - bias[${j}].y)*scale[${j}].y; l += base[${k + 2}].x*(c.z - bias[${j}].z)*scale[${j}].z; } `; } str += ` vec3 basecolor = (texture${gl2 ? '' : '2D'}(plane0, v_texcoord).xyz - bias[0])*scale[0]; return l*vec4(basecolor, 1); } `; return str; } } class RGB { static render(njpegs, gl2) { let str = ` vec4 render(vec3 base[np1]) { vec4 rgb = vec4(0, 0, 0, 1);`; for (let j = 0; j < njpegs; j++) { str += ` { vec4 c = texture${gl2 ? '' : '2D'}(plane${j}, v_texcoord); rgb.x += base[${j}].x*(c.x - bias[${j}].x)*scale[${j}].x; rgb.y += base[${j}].y*(c.y - bias[${j}].y)*scale[${j}].y; rgb.z += base[${j}].z*(c.z - bias[${j}].z)*scale[${j}].z; } `; } str += ` return rgb; } `; return str; } } class MRGB { static render(njpegs, gl2) { let str = ` vec4 render(vec3 base[np1]) { vec3 rgb = base[0]; vec4 c; vec3 r; `; for (let j = 0; j < njpegs; j++) { str += ` c = texture${gl2 ? '' : '2D'}(plane${j}, v_texcoord); r = (c.xyz - bias[${j}])* scale[${j}]; rgb += base[${j}*3+1]*r.x; rgb += base[${j}*3+2]*r.y; rgb += base[${j}*3+3]*r.z; `; } str += ` return vec4(rgb, 1); } `; return str; } } class MYCC { static render(njpegs, ny1, gl2) { let str = ` vec3 toRgb(vec3 ycc) { vec3 rgb; rgb.g = ycc.r + ycc.b/2.0; rgb.b = ycc.r - ycc.b/2.0 - ycc.g/2.0; rgb.r = rgb.b + ycc.g; return rgb; } vec4 render(vec3 base[np1]) { vec3 rgb = base[0]; vec4 c; vec3 r; `; for (let j = 0; j < njpegs; j++) { str += ` c = texture${gl2 ? '' : '2D'}(plane${j}, v_texcoord); r = (c.xyz - bias[${j}])* scale[${j}]; `; if (j < ny1) { str += ` rgb.x += base[${j}*3+1].x*r.x; rgb.y += base[${j}*3+2].y*r.y; rgb.z += base[${j}*3+3].z*r.z; `; } else { str += ` rgb.x += base[${j}*3+1].x*r.x; rgb.x += base[${j}*3+2].x*r.y; rgb.x += base[${j}*3+3].x*r.z; `; } } str += ` return vec4(toRgb(rgb), 1); } `; return str; } } /* FIXME only HSH is exported, is this class complete? */ /* PTM utility functions */ class PTM { /* @param {Array} v expects light direction as [x, y, z] */ static lightWeights(v) { let b = [1.0, v[0], v[1], v[0] * v[0], v[0] * v[1], v[1] * v[1]]; let base = new Float32Array(18); for (let i = 0; i < 18; i++) base[3 * i] = base[3 * i + 1] = base[3 * i + 2] = b[i]; return base; } } /* HSH utility functions */ class HSH { static minElevation = 0.15; /* @param {Array} v expects light direction as [x, y, z] */ static lightWeights(v) { let PI = 3.1415; let phi = Math.atan2(v[1], v[0]); if (phi < 0) phi = 2 * PI + phi; let theta = Math.min(Math.acos(v[2]), PI / 2 - this.minElevation); let cosP = Math.cos(phi); let cosT = Math.cos(theta); let cosT2 = cosT * cosT; let b = [ 1.0 / Math.sqrt(2 * PI), Math.sqrt(6 / PI) * (cosP * Math.sqrt(cosT - cosT2)), Math.sqrt(3 / (2 * PI)) * (-1 + 2 * cosT), Math.sqrt(6 / PI) * (Math.sqrt(cosT - cosT2) * Math.sin(phi)), Math.sqrt(30 / PI) * (Math.cos(2 * phi) * (-cosT + cosT2)), Math.sqrt(30 / PI) * (cosP * (-1 + 2 * cosT) * Math.sqrt(cosT - cosT2)), Math.sqrt(5 / (2 * PI)) * (1 - 6 * cosT + 6 * cosT2), Math.sqrt(30 / PI) * ((-1 + 2 * cosT) * Math.sqrt(cosT - cosT2) * Math.sin(phi)), Math.sqrt(30 / PI) * ((-cosT + cosT2) * Math.sin(2 * phi)) ]; let base = new Float32Array(27); for (let i = 0; i < 27; i++) base[3 * i] = base[3 * i + 1] = base[3 * i + 2] = b[i]; return base; } } class SH { /* @param {Array} v expects light direction as [x, y, z] */ static lightWeights(v) { let PI = 3.1415; let A = 0.5 * Math.sqrt(3.0 / PI); let B = 0.5 * Math.sqrt(15 / PI); let b = [ 0.5 / Math.sqrt(PI), A * v[0], A * v[2], A * v[1], B * v[0] * v[1], B * v[0] * v[2], 0.5 * Math.sqrt(5 / PI) * (3 * v[2] * v[2] - 1), B * v[1] * v[2], 0.5 * B * (v[1] * v[1] - v[0] * v[0]) ]; let base = new Float32Array(27); for (let i = 0; i < 27; i++) base[3 * i] = base[3 * i + 1] = base[3 * i + 2] = b[i]; return base; } } class RBF { /* @param {Array} v expects light direction as [x, y, z] */ static lightWeights(lpos, shader) { let weights = RBF.rbf(lpos, shader); let np = shader.nplanes; let lweights = new Float32Array((np + 1) * 3); for (let p = 0; p < np + 1; p++) { for (let k = 0; k < 3; k++) { for (let l = 0; l < weights.length; l++) { let o = shader.baseLightOffset(p, weights[l][0], k); lweights[3 * p + k] += weights[l][1] * shader.basis[o]; } } } return lweights; } static rbf(lpos, shader) { let radius = 1 / (shader.sigma * shader.sigma); let weights = new Array(shader.ndimensions); //compute rbf weights let totw = 0.0; for (let i = 0; i < weights.length; i++) { let dx = shader.lights[i * 3 + 0] - lpos[0]; let dy = shader.lights[i * 3 + 1] - lpos[1]; let dz = shader.lights[i * 3 + 2] - lpos[2]; let d2 = dx * dx + dy * dy + dz * dz; let w = Math.exp(-radius * d2); weights[i] = [i, w]; totw += w; } for (let i = 0; i < weights.length; i++) weights[i][1] /= totw; //pick only most significant and renormalize let count = 0; totw = 0.0; for (let i = 0; i < weights.length; i++) { if (weights[i][1] > 0.001) { weights[count++] = weights[i]; totw += weights[i][1]; } } weights = weights.slice(0, count); for (let i = 0; i < weights.length; i++) weights[i][1] /= totw; return weights; } } class BLN { static lightWeights(lpos, shader) { let np = shader.nplanes; let s = Math.abs(lpos[0]) + Math.abs(lpos[1]) + Math.abs(lpos[2]); //rotate 45 deg. let x = (lpos[0] + lpos[1]) / s; let y = (lpos[1] - lpos[0]) / s; x = (x + 1.0) / 2.0; y = (y + 1.0) / 2.0; x = x * (shader.resolution - 1.0); y = y * (shader.resolution - 1.0); let sx = Math.min(shader.resolution - 2, Math.max(0, Math.floor(x))); let sy = Math.min(shader.resolution - 2, Math.max(0, Math.floor(y))); let dx = x - sx; let dy = y - sy; //bilinear interpolation coefficients. let s00 = (1 - dx) * (1 - dy); let s10 = dx * (1 - dy); let s01 = (1 - dx) * dy; let s11 = dx * dy; let lweights = new Float32Array((np + 1) * 3); //TODO optimize away basePixel for (let p = 0; p < np + 1; p++) { for (let k = 0; k < 3; k++) { let o00 = shader.basePixelOffset(p, sx, sy, k); let o10 = shader.basePixelOffset(p, sx + 1, sy, k); let o01 = shader.basePixelOffset(p, sx, sy + 1, k); let o11 = shader.basePixelOffset(p, sx + 1, sy + 1, k); lweights[3 * p + k] = s00 * shader.basis[o00] + s10 * shader.basis[o10] + s01 * shader.basis[o01] + s11 * shader.basis[o11]; } } return lweights; } } /** * Color Space Implementations * Each class provides shader code generation for specific color spaces * * @namespace */ /** * LRGB * Luminance/RGB color space implementation * Separates luminance variation from base color */ /** * RGB * RGB color space implementation * Direct RGB coefficient manipulation */ /** * MRGB * Modified RGB color space implementation * PCA-based color space transformation */ /** * MYCC * Modified YCC color space implementation * Luminance/chrominance separation with custom transformation */ /** * Basis Function Implementations * Each class provides coefficient computation for different basis types * * @namespace */ /** * PTM * Polynomial Texture Map basis * Uses 6 coefficients per color channel */ /** * HSH * Hemispherical Harmonics basis * Uses 9 coefficients per color channel */ /** * SH * Spherical Harmonics basis * Uses 9 coefficients per color channel */ /** * RBF * Radial Basis Functions * Uses measured light directions with Gaussian interpolation */ /** * BLN * Bilinear interpolation * Uses grid-based interpolation of light directions */ /** * Example usage with various configurations: * ```javascript * // PTM shader * const ptmShader = new ShaderRTI({ * type: 'ptm', * colorspace: 'rgb' * }); * * // HSH shader with normals * const hshShader = new ShaderRTI({ * type: 'hsh', * colorspace: 'lrgb', * mode: 'normals' * }); * * // RBF shader * const rbfShader = new ShaderRTI({ * type: 'rbf', * colorspace: 'mrgb', * sigma: 1.0 * }); * ``` */ export { ShaderRTI, HSH }