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

346 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)