9.5 KiB
9.5 KiB
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_textureSizeuniform - Замінити
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
╰───────────────╯