import { Color } from './Colormap.js'; import { ShaderFilter } from './ShaderFilter.js' import { Util } from './Util' // vector field https://www.shadertoy.com/view/4s23DG // isolines https://www.shadertoy.com/view/Ms2XWc /** * @typedef {Object} ShaderFilterVectorGlyph~Options * Configuration options for glyph-based vector field visualization * @property {number[]} [inDomain=[]] - Input value range [min, max] for magnitude mapping * @property {number} [maxSteps=256] - Number of discrete steps in the colormap texture * @property {number[]} [glyphColor=[0.0, 0.0, 0.0, 1.0]] - RGBA color for glyphs when using 'col' mode * @property {number} [glyphsStride=80] - Horizontal spacing between glyphs in the sprite sheet * @property {number[]} [glyphsSize=[304, 64]] - Dimensions of the glyph sprite sheet [width, height] */ /** * @typedef {Object} ShaderFilterVectorGlyph~Modes * Available visualization modes * @property {string} normalize - Glyph size normalization ('on'|'off') * @property {string} glyph - Glyph coloring mode ('mag'|'col') * @property {string} field - Background field visualization ('none'|'mag') */ /** * ShaderFilterVectorGlyph implements sprite-based vector field visualization. * Uses pre-rendered glyphs from an SVG sprite sheet for high-quality vector field representation. * * @class * @extends ShaderFilter * @classdesc A shader filter that implements sprite-based vector field visualization. * * Features: * - SVG glyph-based vector field visualization * - Magnitude-dependent glyph selection * - Custom glyph coloring * - Optional vector normalization * - Background field visualization * - Smooth rotation and scaling * * Technical Implementation: * - Sprite sheet-based glyph rendering * - Dynamic glyph rotation and scaling * - Automatic magnitude mapping * - Alpha-based glyph composition * - WebGL texture management * * GLSL Implementation Constants: * - GLYPH_TILE_SIZE: Spacing between glyphs (16.0) * - ISQRT2: 1/sqrt(2) for magnitude normalization * * */ class ShaderFilterVectorGlyph extends ShaderFilter { /** * Creates a new glyph-based vector field visualization filter * @param {ColorScale} colorscale - Colorscale for magnitude mapping * @param {string} glyphsUrl - URL to SVG sprite sheet containing glyphs * @param {ShaderFilterVectorGlyph~Options} [options] - Configuration options * @throws {Error} If inDomain is invalid or glyphsUrl is empty * * @example * ```javascript * // Create with custom options * const filter = new ShaderFilterVectorGlyph(colorscale, 'glyphs.svg', { * inDomain: [0, 1], * glyphsSize: [304, 64], * glyphsStride: 80, * glyphColor: [0, 0, 0, 1] * }); * ``` */ constructor(colorscale, glyphsUrl, options) { super(options); options = Object.assign({ inDomain: [], maxSteps: 256, glyphColor: [0.0, 0.0, 0.0, 1.0], glyphsStride: 80, glyphsSize: [304, 64] }, options); Object.assign(this, options); if (this.inDomain.length != 2 && this.inDomain[1] <= this.inDomain[0]) { throw Error("inDomain bad format"); } this.glyphsUrl = glyphsUrl; if (this.glyphsUrl.length == 0) throw Error("glyphUrl is empty: no items to display"); this.colorscale = colorscale; if (this.inDomain.length == 0) this.inDomain = this.colorscale.rangeDomain(); const cscaleDomain = this.colorscale.rangeDomain(); const scale = Math.sqrt((this.inDomain[1] * this.inDomain[1] + this.inDomain[0] * this.inDomain[0]) / (cscaleDomain[1] * cscaleDomain[1] + cscaleDomain[0] * cscaleDomain[0])); const bias = 0.0; const gap = this.glyphsStride - this.glyphsSize[1]; const glyphCount = Math.round((this.glyphsSize[0] + gap) / this.glyphsStride); this.modes = { normalize: [ { id: 'off', enable: true, src: `const bool ${this.modeName('glyphNormalize')} = false;` }, { id: 'on', enable: false, src: `const bool ${this.modeName('glyphNormalize')} = true;` } ], glyph: [ { id: 'mag', enable: true, src: `const int ${this.modeName('glyphColor')} = 0;` }, { id: 'col', enable: false, src: `const int ${this.modeName('glyphColor')} = 1;` } ], field: [ { id: 'none', enable: true, src: `const int ${this.modeName('fieldColor')} = 0;` }, { id: 'mag', enable: false, src: `const int ${this.modeName('fieldColor')} = 1;` } ] }; this.samplers = [{ name: `${this.samplerName('colormap')}` }, { name: `${this.samplerName('glyphs')}` }]; this.uniforms[this.uniformName('glyph_color')] = { type: 'vec4', needsUpdate: true, size: 4, value: this.glyphColor }; this.uniforms[this.uniformName('glyph_count')] = { type: 'float', needsUpdate: true, size: 1, value: glyphCount }; this.uniforms[this.uniformName('glyph_wh')] = { type: 'float', needsUpdate: true, size: 1, value: this.glyphsSize[1] }; this.uniforms[this.uniformName('glyph_stride')] = { type: 'float', needsUpdate: true, size: 1, value: this.glyphsStride }; this.uniforms[this.uniformName('low_color')] = { type: 'vec4', needsUpdate: true, size: 4, value: this.colorscale.lowColor.value() }; this.uniforms[this.uniformName('high_color')] = { type: 'vec4', needsUpdate: true, size: 4, value: this.colorscale.highColor.value() }; this.uniforms[this.uniformName('scale')] = { type: 'float', needsUpdate: true, size: 1, value: scale }; this.uniforms[this.uniformName('bias')] = { type: 'float', needsUpdate: true, size: 1, value: bias }; } /** * Creates textures for glyphs and colormap. * * Implementation details: * 1. Glyph Texture: * - Rasterizes SVG to image buffer * - Creates and configures texture * - Sets up linear filtering * * 2. Colormap Texture: * - Samples colorscale * - Creates 1D RGBA texture * - Configures appropriate filtering * * @param {WebGLRenderingContext} gl - WebGL context * @returns {Promise<void>} * @private */ async createTextures(gl) { // Glyphs const glyphsBuffer = await Util.rasterizeSVG(this.glyphsUrl, this.glyphsSize); const glyphsTex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, glyphsTex); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_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); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, glyphsBuffer); this.getSampler('glyphs').tex = glyphsTex; // Colormap const colormap = this.colorscale.sample(this.maxSteps); let textureFilter = gl.LINEAR; if (this.colorscale.type == 'bar') { textureFilter = gl.NEAREST; } const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, textureFilter); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, textureFilter); 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); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.maxSteps, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, colormap.buffer); this.getSampler('colormap').tex = tex; } /** * Generates GLSL code for glyph-based vector field visualization. * * Shader Features: * - Tile-based glyph placement * - Dynamic glyph rotation * - Magnitude-based glyph selection * - Alpha-based composition * - Multiple visualization modes * * @param {WebGLRenderingContext} gl - WebGL context * @returns {string} GLSL function definition * @private */ fragDataSrc(gl) { return ` // 2D vector glyph visualization const float GLYPH_TILE_SIZE = 16.0; const float ISQRT2 = 0.70710678118; // 1/sqrt(2) // Computes the center pixel of the tile containing pixel pos vec2 glyphTileCenterCoord(vec2 pos) { return (floor(pos / GLYPH_TILE_SIZE) + 0.5) * GLYPH_TILE_SIZE; } float glyph(vec2 p, vec2 v) { if (${this.modeName('glyphNormalize')}) v = normalize(v); // Make everything relative to the center, which may be fractional p -= glyphTileCenterCoord(p); float mag_v = length(v), mag_p = length(p); if (mag_v > 0.0) { // Non-zero velocity case vec2 dir_v = normalize(v); float level = floor((1.0-mag_v*ISQRT2) * ${this.uniformName('glyph_count')}); level = min(level, ${this.uniformName('glyph_count')} - 1.0); mat2 rotm = mat2( dir_v[1], dir_v[0], // first column -dir_v[0], dir_v[1] // second column ); float scaleToGlyph = ${this.uniformName('glyph_wh')} / GLYPH_TILE_SIZE; vec2 pp = rotm * p; // p on axys with origin in tile center and aligned with direction dir_v pp += vec2(GLYPH_TILE_SIZE * 0.5, GLYPH_TILE_SIZE * 0.5); // pp in [0, GLYPH_TILE_SIZE] pp *= scaleToGlyph; // pp in [0, glyph_wh] pp.x += level * ${this.uniformName('glyph_stride')}; // apply stride pp.y = ${this.uniformName('glyph_wh')} - pp.y - 1.0; // invert y-axis //vec4 g = texelFetch(${this.samplerName('glyphs')}, ivec2(pp), 0); float w = ${this.uniformName('glyph_stride')}*(${this.uniformName('glyph_count')} -1.0) + ${this.uniformName('glyph_wh')}; float h = ${this.uniformName('glyph_wh')}; vec2 ppnorm = pp/vec2(w,h); vec4 g = texture(${this.samplerName('glyphs')}, ppnorm); return 1.0-g.a; } else { // Signed distance from the center point return mag_p; } } vec4 lookupColormap(float cv) { if(cv >= 1.0) return ${this.uniformName('high_color')}; else if(cv <= 0.0) return ${this.uniformName('low_color')}; return texture(${this.samplerName('colormap')}, vec2(cv, 0.5)); } vec4 ${this.functionName()}(vec4 col){ if(col.a == 0.0) return col; vec2 p = v_texcoord*tileSize; // point in pixel vec2 pc_coord = glyphTileCenterCoord(p)/tileSize; // center coordinate vec4 pc_val = texture(kd, pc_coord); // [0..1] - lookup color in center float s = 2.0; float b = -1.0; vec2 uvc = vec2(pc_val.x*s+b, pc_val.y*s+b); // [-1..1] vec2 uvr = vec2(col.r*s+b, col.g*s+b); // [-1..1] // Colors float vc = length(uvc)*ISQRT2; float cvc = vc*${this.uniformName('scale')} + ${this.uniformName('bias')}; float vr = length(uvr)*ISQRT2; float cvr = vr*${this.uniformName('scale')} + ${this.uniformName('bias')}; vec4 cmapc = lookupColormap(cvc); vec4 cmapr = lookupColormap(cvr); // Glyph float glyph_dist = glyph(p, uvc); vec4 glyph_col = cmapc; vec4 field_col = vec4(0.0, 0.0, 0.0, 0.0); switch (${this.modeName('glyphColor')}) { case 0: glyph_col = cmapc; break; case 1: glyph_col = ${this.uniformName('glyph_color')}; break; } switch (${this.modeName('fieldColor')}) { case 0: field_col = vec4(0.0, 0.0, 0.0, 0.0); break; case 1: field_col = cmapr; break; } float t = clamp(glyph_dist, 0.0, 1.0); return mix(glyph_col, field_col, t); }`; } } export { ShaderFilterVectorGlyph }