Files
rfcp/docs/devlog/gpu_supp/RFCP-WebGL-Smooth-Coverage-Task.md
2026-02-07 12:56:25 +02:00

9.5 KiB
Raw Blame History

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:

// ЗАМІСТЬ:
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:

const textureSizeLocation = gl.getUniformLocation(program, 'u_textureSize');
gl.uniform2f(textureSizeLocation, textureWidth, textureHeight);

Етап 2: Production Implementation (1-2 години)

9-tap Catmull-Rom Shader

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 зміни

// 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

Поточний формат (перевірити)

// 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 (краща точність)

// Якщо браузер підтримує 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
  2. Inigo Quilez - Better Texture Filtering
  3. 2D Catmull-Rom in 4 samples - Shadertoy
  4. mapbox-gl-interpolate-heatmap
  5. NVIDIA GPU Gems 2 - Fast Third-Order Texture Filtering