# RFCP: WebGL Radial Gradients Coverage Layer ## Мета Переробити WebGL coverage layer з texture-based підходу на **radial gradients** — як працює Canvas GeographicHeatmap, але на GPU. ## Чому radial gradients краще для візуалізації **Texture-based (поточний):** - Кожна точка = 1 pixel в grid - Nearest neighbor fill → blocky квадрати - Навіть з smoothstep — видно grid структуру - ✅ Добре для: terrain detail, точні значення - ❌ Погано для: красива візуалізація **Radial gradients (Canvas heatmap):** - Кожна точка = круг з radial falloff - Smooth blending між точками - Природній вигляд coverage - ✅ Добре для: красива візуалізація, презентації - ❌ Погано для: точні значення (blending спотворює) ## Архітектура WebGL Radial Gradients ### Підхід: Multi-pass additive blending ``` Pass 1-N: Для кожної точки (або batch точок) ├── Малюємо full-screen quad ├── Fragment shader: radial falloff від центру точки ├── Output: (weight * value, weight, 0, 1) └── Blending: GL_ONE, GL_ONE (additive) Final Pass: ├── Читаємо accumulated texture ├── Normalize: value = R / G (weighted average) └── Apply colormap ``` ### Альтернатива: Single-pass з texture atlas Замість N проходів, закодувати всі точки в texture і в одному fragment shader пройтись по всіх: ```glsl // Fragment shader uniform sampler2D u_points; // texture з точками: (lat, lon, rsrp, radius) uniform int u_pointCount; void main() { vec2 worldPos = getWorldPosition(v_uv); float totalWeight = 0.0; float totalValue = 0.0; for (int i = 0; i < MAX_POINTS; i++) { if (i >= u_pointCount) break; vec4 point = texelFetch(u_points, ivec2(i, 0), 0); vec2 pointPos = point.xy; float rsrp = point.z; float radius = point.w; float dist = distance(worldPos, pointPos); float weight = smoothstep(radius, 0.0, dist); totalWeight += weight; totalValue += weight * rsrp; } if (totalWeight < 0.001) discard; float avgRsrp = totalValue / totalWeight; vec3 color = rsrpToColor(avgRsrp); gl_FragColor = vec4(color, smoothstep(0.0, 0.1, totalWeight)); } ``` **Проблема:** Loop по 6,675 точках в кожному fragment = дуже повільно. ### Рекомендований підхід: Batched additive blending ``` 1. Створити offscreen framebuffer (float texture) 2. Для кожної точки (або batch по 100-500): - Малювати quad розміром з radius точки - Additive blend: (weight * rsrp, weight) 3. Final pass: normalize + colormap ``` Це як Mapbox heatmap працює. --- ## Імплементація ### Крок 1: Створити offscreen framebuffer ```typescript // Accumulation texture (RG float for weighted sum) const accumTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, accumTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32F, width, height, 0, gl.RG, gl.FLOAT, null); const framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, accumTexture, 0); ``` **Примітка:** Потрібен `EXT_color_buffer_float` extension для float framebuffer. ### Крок 2: Point rendering shader **Vertex shader:** ```glsl attribute vec2 a_position; // quad vertices attribute vec2 a_pointCenter; // point lat/lon (instanced) attribute float a_pointRsrp; // point RSRP (instanced) attribute float a_pointRadius; // point radius in pixels (instanced) uniform mat4 u_matrix; // world to clip transform varying vec2 v_localPos; // position relative to point center varying float v_rsrp; void main() { // Expand quad around point center vec2 worldPos = a_pointCenter + a_position * a_pointRadius; gl_Position = u_matrix * vec4(worldPos, 0.0, 1.0); v_localPos = a_position; // -1 to 1 v_rsrp = a_pointRsrp; } ``` **Fragment shader:** ```glsl precision highp float; varying vec2 v_localPos; varying float v_rsrp; void main() { // Radial distance from center (0 at center, 1 at edge) float dist = length(v_localPos); // Discard outside circle if (dist > 1.0) discard; // Radial falloff (smooth at edges) float weight = 1.0 - smoothstep(0.0, 1.0, dist); // Or gaussian: weight = exp(-dist * dist * 2.0); // Output: (weight * normalized_rsrp, weight) float normalizedRsrp = (v_rsrp + 130.0) / 80.0; // -130 to -50 → 0 to 1 gl_FragColor = vec4(weight * normalizedRsrp, weight, 0.0, 1.0); } ``` ### Крок 3: Final compositing shader ```glsl precision highp float; uniform sampler2D u_accumTexture; varying vec2 v_uv; vec3 rsrpToColor(float t) { // t: 0 = weak (red), 1 = strong (cyan) if (t < 0.25) return mix(vec3(1.0, 0.0, 0.0), vec3(1.0, 0.5, 0.0), t / 0.25); if (t < 0.5) return mix(vec3(1.0, 0.5, 0.0), vec3(1.0, 1.0, 0.0), (t - 0.25) / 0.25); if (t < 0.75) return mix(vec3(1.0, 1.0, 0.0), vec3(0.0, 1.0, 0.0), (t - 0.5) / 0.25); return mix(vec3(0.0, 1.0, 0.0), vec3(0.0, 1.0, 1.0), (t - 0.75) / 0.25); } void main() { vec2 accum = texture2D(u_accumTexture, v_uv).rg; float totalValue = accum.r; float totalWeight = accum.g; // No coverage if (totalWeight < 0.001) discard; // Weighted average RSRP float avgRsrp = totalValue / totalWeight; // Color mapping vec3 color = rsrpToColor(avgRsrp); // Alpha based on weight (fade at edges) float alpha = smoothstep(0.0, 0.1, totalWeight) * 0.85; gl_FragColor = vec4(color, alpha); } ``` ### Крок 4: Rendering loop ```typescript function render() { const canvas = canvasRef.current; const gl = glRef.current; // 1. Position canvas over map const nw = map.latLngToLayerPoint([bounds.maxLat, bounds.minLon]); const se = map.latLngToLayerPoint([bounds.minLat, bounds.maxLon]); canvas.style.transform = `translate(${nw.x}px, ${nw.y}px)`; canvas.style.width = `${se.x - nw.x}px`; canvas.style.height = `${se.y - nw.y}px`; // 2. Clear accumulation buffer gl.bindFramebuffer(gl.FRAMEBUFFER, accumFramebuffer); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); // 3. Render points with additive blending gl.useProgram(pointProgram); gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE); // Additive // Set uniforms (matrix, etc.) const matrix = calculateWorldToClipMatrix(bounds, canvas.width, canvas.height); gl.uniformMatrix4fv(u_matrix, false, matrix); // Draw all points (instanced if supported, or batched) drawPoints(gl, points); // 4. Final composite pass gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.useProgram(compositeProgram); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // Normal blend gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, accumTexture); drawFullscreenQuad(gl); } ``` --- ## Оптимізації ### 1. Instanced rendering (якщо підтримується) ```typescript const ext = gl.getExtension('ANGLE_instanced_arrays'); if (ext) { // Use instanced rendering - draw all points in one call ext.drawArraysInstancedANGLE(gl.TRIANGLE_STRIP, 0, 4, points.length); } ``` ### 2. Spatial culling Малювати тільки точки що потрапляють у viewport: ```typescript const visiblePoints = points.filter(p => { const screenPos = map.latLngToContainerPoint([p.lat, p.lon]); return screenPos.x > -radius && screenPos.x < canvas.width + radius && screenPos.y > -radius && screenPos.y < canvas.height + radius; }); ``` ### 3. Dynamic radius based on zoom ```typescript const zoom = map.getZoom(); const metersPerPixel = 40075016.686 * Math.cos(centerLat * Math.PI / 180) / Math.pow(2, zoom + 8); const radiusPixels = (settings.resolution * 1.5) / metersPerPixel; ``` ### 4. Resolution scaling На низьких zoom рівнях, рендерити в менший framebuffer і upscale: ```typescript const scale = zoom < 10 ? 0.5 : zoom < 12 ? 0.75 : 1.0; const fbWidth = Math.round(canvas.width * scale); const fbHeight = Math.round(canvas.height * scale); ``` --- ## Порівняння з поточним texture-based | Аспект | Texture-based | Radial gradients | |--------|---------------|------------------| | Візуалізація | Blocky | Smooth | | Terrain detail | Добре | Менш точно | | Performance | Швидко (1 draw call) | Повільніше (N points) | | Memory | Texture size | Framebuffer + points | | Код складність | Середня | Висока | --- ## Чеклист імплементації ### Phase 1: Basic setup - [ ] Створити новий файл `WebGLRadialCoverageLayer.tsx` - [ ] Setup WebGL context з float extensions - [ ] Створити accumulation framebuffer - [ ] Базовий vertex/fragment shader для точок ### Phase 2: Point rendering - [ ] Implement point quad rendering - [ ] Radial falloff function - [ ] Additive blending - [ ] Test з кількома точками ### Phase 3: Compositing - [ ] Final pass shader - [ ] Weighted average calculation - [ ] Color mapping - [ ] Alpha/transparency ### Phase 4: Integration - [ ] Map positioning (як в поточному WebGL layer) - [ ] Map event listeners (move/zoom) - [ ] Opacity control - [ ] Toggle в UI ### Phase 5: Optimization - [ ] Instanced rendering - [ ] Spatial culling - [ ] Dynamic radius - [ ] Resolution scaling --- ## Fallback Якщо WebGL radial не працює (older GPU, missing extensions): - Fallback до Canvas GeographicHeatmap - Або до поточного texture-based WebGL --- ## Референси 1. [Mapbox GL Heatmap implementation](https://github.com/mapbox/mapbox-gl-js/blob/main/src/render/draw_heatmap.js) 2. [deck.gl HeatmapLayer](https://deck.gl/docs/api-reference/aggregation-layers/heatmap-layer) 3. [WebGL additive blending](https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html)