# RFCP v3.10.5: WebGL Smooth Coverage Implementation ## Контекст проблеми **Поточний стан:** - Backend повертає grid точок з lat/lon/RSRP (50m = 6,675 pts, 200m = 1,975 pts) - WebGL texture-based rendering: points → texture → GL_LINEAR → colormap - **Проблема:** Видимі grid squares/pixelation, особливо при zoom in або sparse grids (200m) **Причина:** - `GL_LINEAR` дає тільки C0 continuity (значення співпадають на краях, але похідні — ні) - Це створює видимі "шви" між клітинками ## Рішення з ресерчу ### Ключовий інсайт **Catmull-Rom spline interpolation** дає C1 continuity (smooth derivatives) І проходить через exact data values (на відміну від B-spline який blurs peaks). **9-tap Catmull-Rom** замість `texture2D()`: - 9 texture fetches замість 1 - ~0.32ms vs ~0.30ms на GTX 980 при 1920×1080 - Для нашої ~80×85 текстури — практично безкоштовно ### Критичне правило **Інтерполювати RAW RSRP values ПЕРЕД colormap!** - ❌ Неправильно: texture → colormap → interpolate (muddy colors) - ✅ Правильно: texture → interpolate → colormap (clean gradients) --- ## Етап 1: Quick Fix (30 хвилин) ### Smoothstep coordinate remapping Найшвидший спосіб прибрати grid edges — одна зміна в shader: ```glsl // ЗАМІСТЬ: vec4 texColor = texture2D(u_texture, v_uv); // ВИКОРИСТАТИ: vec4 textureSmooth(sampler2D tex, vec2 uv, vec2 texSize) { vec2 p = uv * texSize + 0.5; vec2 i = floor(p); vec2 f = p - i; f = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); // quintic hermite return texture2D(tex, (i + f - 0.5) / texSize); } // В main(): vec4 texColor = textureSmooth(u_texture, v_uv, u_textureSize); ``` **Що це дає:** - C2 continuity з одним texture read - Прибирає видимі grid edges - Мінімальний positional bias **Потрібно додати uniform:** ```javascript const textureSizeLocation = gl.getUniformLocation(program, 'u_textureSize'); gl.uniform2f(textureSizeLocation, textureWidth, textureHeight); ``` --- ## Етап 2: Production Implementation (1-2 години) ### 9-tap Catmull-Rom Shader ```glsl precision highp float; uniform sampler2D u_texture; uniform vec2 u_textureSize; uniform float u_opacity; varying vec2 v_uv; // Catmull-Rom 9-tap interpolation // Source: TheRealMJP's gist (108 GitHub stars) vec4 SampleTextureCatmullRom(sampler2D tex, vec2 uv, vec2 texSize) { vec2 samplePos = uv * texSize; vec2 texPos1 = floor(samplePos - 0.5) + 0.5; vec2 f = samplePos - texPos1; // Catmull-Rom weights vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f)); vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f); vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f)); vec2 w3 = f * f * (-0.5 + 0.5 * f); // Combine weights for optimized sampling vec2 w12 = w1 + w2; vec2 offset12 = w2 / (w1 + w2); // Compute texture coordinates vec2 texPos0 = (texPos1 - 1.0) / texSize; vec2 texPos3 = (texPos1 + 2.0) / texSize; vec2 texPos12 = (texPos1 + offset12) / texSize; // 9 texture fetches (optimized from 16) vec4 result = vec4(0.0); result += texture2D(tex, vec2(texPos0.x, texPos0.y)) * w0.x * w0.y; result += texture2D(tex, vec2(texPos12.x, texPos0.y)) * w12.x * w0.y; result += texture2D(tex, vec2(texPos3.x, texPos0.y)) * w3.x * w0.y; result += texture2D(tex, vec2(texPos0.x, texPos12.y)) * w0.x * w12.y; result += texture2D(tex, vec2(texPos12.x, texPos12.y)) * w12.x * w12.y; result += texture2D(tex, vec2(texPos3.x, texPos12.y)) * w3.x * w12.y; result += texture2D(tex, vec2(texPos0.x, texPos3.y)) * w0.x * w3.y; result += texture2D(tex, vec2(texPos12.x, texPos3.y)) * w12.x * w3.y; result += texture2D(tex, vec2(texPos3.x, texPos3.y)) * w3.x * w3.y; return result; } // RSRP to color mapping (cyan -> green -> yellow -> orange -> red) vec3 rsrpToColor(float rsrp) { // rsrp: normalized 0.0 (weak, -110dBm) to 1.0 (strong, -50dBm) // Color stops: red -> orange -> yellow -> green -> cyan vec3 c0 = vec3(1.0, 0.0, 0.0); // red (weak) vec3 c1 = vec3(1.0, 0.5, 0.0); // orange vec3 c2 = vec3(1.0, 1.0, 0.0); // yellow vec3 c3 = vec3(0.0, 1.0, 0.0); // green vec3 c4 = vec3(0.0, 1.0, 1.0); // cyan (strong) float t = clamp(rsrp, 0.0, 1.0); if (t < 0.25) { return mix(c0, c1, t / 0.25); } else if (t < 0.5) { return mix(c1, c2, (t - 0.25) / 0.25); } else if (t < 0.75) { return mix(c2, c3, (t - 0.5) / 0.25); } else { return mix(c3, c4, (t - 0.75) / 0.25); } } void main() { // 1. Sample with Catmull-Rom interpolation (RAW value) vec4 texColor = SampleTextureCatmullRom(u_texture, v_uv, u_textureSize); float rsrpNormalized = texColor.r; // 2. Discard if no coverage (validity check) if (rsrpNormalized < 0.01) { discard; } // 3. Apply colormap AFTER interpolation vec3 color = rsrpToColor(rsrpNormalized); // 4. Smooth boundary fading (optional) float boundaryAlpha = smoothstep(0.01, 0.05, rsrpNormalized); gl_FragColor = vec4(color, boundaryAlpha * u_opacity); } ``` ### JavaScript зміни ```javascript // 1. Vertex shader (без змін) const vertexShaderSource = ` attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_uv; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_uv = a_texCoord; } `; // 2. При створенні texture — зберегти розміри const textureWidth = gridWidth; const textureHeight = gridHeight; // 3. Передати uniform const textureSizeLocation = gl.getUniformLocation(program, 'u_textureSize'); if (textureSizeLocation) { gl.uniform2f(textureSizeLocation, textureWidth, textureHeight); } else { console.error('[WebGL] u_textureSize uniform NOT FOUND!'); } // 4. Texture filtering — можна залишити LINEAR для fallback 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); ``` --- ## Етап 3: Texture Data Format ### Поточний формат (перевірити) ```javascript // Normalized RSRP value (0-255 mapped to 0.0-1.0 in shader) const normalized = (rsrp - minRsrp) / (maxRsrp - minRsrp); const value = Math.round(normalized * 255); // Store in R channel textureData[idx] = value; // R = normalized RSRP textureData[idx + 1] = value; // G (можна використати для validity mask) textureData[idx + 2] = value; // B textureData[idx + 3] = 255; // A = fully opaque ``` ### Альтернатива: Float texture (краща точність) ```javascript // Якщо браузер підтримує OES_texture_float const ext = gl.getExtension('OES_texture_float'); if (ext) { const floatData = new Float32Array(width * height); for (const point of points) { const normalized = (point.rsrp - minRsrp) / (maxRsrp - minRsrp); floatData[gridY * width + gridX] = normalized; } gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0, gl.LUMINANCE, gl.FLOAT, floatData); } ``` --- ## Чеклист імплементації ### Phase 1: Quick Test (Smoothstep) - [ ] Додати `u_textureSize` uniform - [ ] Замінити `texture2D()` на `textureSmooth()` - [ ] Тест на 50m і 200m - [ ] Тест zoom in/out ### Phase 2: Production (Catmull-Rom) - [ ] Імплементувати `SampleTextureCatmullRom()` - [ ] Оновити colormap function - [ ] Додати boundary fading - [ ] Тест edge cases (краї текстури) - [ ] Performance benchmark ### Phase 3: Polish - [ ] Видалити старі CSS blur workarounds - [ ] Видалити cellSize multiplication (не потрібно з Catmull-Rom) - [ ] Cleanup debug logs - [ ] Update version to v3.10.5 --- ## Очікуваний результат **До (GL_LINEAR):** ``` ┌───┬───┬───┐ │ A │ B │ C │ ← Видимі краї між клітинками ├───┼───┼───┤ C0 continuity │ D │ E │ F │ └───┴───┴───┘ ``` **Після (Catmull-Rom):** ``` ╭───────────────╮ │ ░░░▒▒▓▓██ │ ← Smooth gradient │ ░░░▒▒▓▓██▓▓ │ C1 continuity │ ░░▒▒▓▓██ │ Exact values at grid points ╰───────────────╯ ``` --- ## Референси 1. [TheRealMJP's 9-tap Catmull-Rom HLSL](https://gist.github.com/TheRealMJP/c83b8c0f46b63f3a88a5986f4fa982b1) 2. [Inigo Quilez - Better Texture Filtering](https://iquilezles.org/articles/texture/) 3. [2D Catmull-Rom in 4 samples - Shadertoy](https://www.shadertoy.com/view/4tyGDD) 4. [mapbox-gl-interpolate-heatmap](https://github.com/vinayakkulkarni/mapbox-gl-interpolate-heatmap) 5. [NVIDIA GPU Gems 2 - Fast Third-Order Texture Filtering](https://developer.nvidia.com/gpugems/gpugems2/part-iii-high-quality-rendering/chapter-20-fast-third-order-texture-filtering)