CASE STUDIESProjekt własny
Symulacja pogody na podstawie danych z API
Tło aplikacji pogodowej miało pokazywać prawdziwe niebo z prognozy, a nie animację. Własny raymarching w GLSL, fizyka chmur z danych Open-Meteo i render, który działa płynnie także na telefonie.
- Kiedy
- 2022–2026
- Rola
- Autor: koncepcja, shadery, fizyka, UI
- Stack
- ReactTypeScriptThree.jsGLSLSassOpen-Meteo
- Linki
- repozytoriumdemo

4 032
miejsco-godzin w kalibracji pokrycia
4
poziomy jakości od desktopu po „potato”
5–9
draw calli na klatkę, bez postprocessingu
Problem
Yet Another Weather App zaczęła się w 2022 roku jako zwykła aplikacja pogodowa. Chciałem, żeby tło nie było ilustracją, tylko niebem policzonym z tej samej prognozy, którą widzi użytkownik: jeśli Open-Meteo mówi o niskim zachmurzeniu 80 procent i burzy, scena ma wyglądać jak zamknięty szary deck z gromem, a nie jak ładna animacja z biblioteki.
Gotowe rozwiązania odpadły po testach. Biblioteka chmur wolumetrycznych dla Three.js liczyła w pełnej rozdzielczości i zabijała średnie telefony, sprite’owe chmury z drei wyglądały płasko i topiły GPU overdrawem, a postprocessing dokładał kolejny pełnoekranowy pass. Do tego dochodził drugi problem: jak uczciwie przełożyć liczby z prognozy (pokrycie w warstwach, CAPE, wysokość warstwy granicznej, wiatr na poziomach ciśnienia) na parametry sceny, żeby model nie przeczył etykiecie „przeważnie bezchmurnie”.
Podejście
Napisałem własny raymarcher w stylu Nubis (technika znana z Horizon Zero Dawn): promień idzie przez sferyczną powłokę nad zakrzywioną Ziemią, w metrach. Gęstość to szum Perlin-Worley kształtujący bryłę, gradient wysokości osobny dla stratusów, cumulusów i cumulonimbusów, remap polem pokrycia i erozja detalu szumem Worleya. Oświetlenie składa się z prawa Beera, efektu „powder”, trójlobowej funkcji fazowej Henyeya-Greensteina i trzech oktaw rozpraszania, a dwustrumieniowe „diffuse floor” pilnuje, żeby pełne zachmurzenie było szare, nie czarne.
Koszt na telefonach zbiłem czterema passami na ułamku canvasu. March rysuje jeden piksel z każdego bloku 2×2 na klatkę, a temporalny resolve reprojektuje historię po kierunku widoku i tnie ją do średniej ± 1,5σ z sąsiednich świeżych tekseli, więc duchy znikają bez rozmycia. Sam march jest adaptacyjny: tanie kroki przez puste powietrze, cofnięcie po trafieniu w chmurę, drobne kroki rzędu 2 procent dystansu. Cztery poziomy jakości dobierają DPR, liczbę kroków, rozmiar szumu 3D i zasięg; drei PerformanceMonitor schodzi poziom niżej, gdy klatki spadają poniżej 75 procent limitu.
Dane z prognozy przechodzą przez własny moduł fizyki chmur: podstawa z poziomu kondensacji (około 125 m na każdy stopień różnicy temperatury i punktu rosy), wierzchołek z warstwy granicznej i CAPE, wysokości warstw z poziomów ciśnienia, a grubość optyczna z grubości warstwy zmieszana z wartością odwróconą z indeksu klarowności. Pokrycie skalibrowałem tak, by liczba z API oznaczała dokładnie taki udział nieba, a nie „jakiś” próg szumu.
Rozwiązanie
- Shader rozbity na moduły: march, density, lighting, resolve. Stałe współdzielone między TypeScriptem a GLSL z jednego eksportu, więc nie rozjeżdżają się przy kolejnych zmianach.
- Dryf chmur w przestrzeni szumu z wiatru na 850 hPa (niskie) i 300 hPa (cirrusy), owinięty okresami kafli, bo float32 po dobie kwantował szum co 520 metrów.
- Bug sterownika ANGLE/D3D11 na Windows: texture() z niejawnymi pochodnymi w pętli zależnej od danych zwracał pusty promień i chmury w pobliżu kamery znikały. Poprawka to textureLod z poziomem 0.
- Model przeczył etykiecie: dla Jēkabpils API podawało „przeważnie bezchmurnie, 26 procent”, a warstwa niska 78 procent. Zmierzyłem to na 4 032 realnych miejsco-godzinach z 42 miejsc i wprowadziłem regułę: żadna warstwa nie przekracza totalu. Zamknięte decki pod totalem poniżej 50 procent spadły z 12 do 0, a korekta objęła 2,4 procent godzin.
- Klatki wydawane przez FrameRateGate co N-tą klatkę rAF zamiast bramki czasowej, która gubiła klatki losowo.
Efekt
Scena działa bez postprocessingu i bez klucza API, w 5–9 draw callach na klatkę. Na desktopie cel chmur to połowa canvasu przy 64 oświetlonych krokach i szumie 128³, na najniższym poziomie jedna trzecia canvasu, 32 kroki i szum 64³, a na „potato” płaski deck. Przebudowa shadera na moduły zamknęła się bit-identycznym wynikiem na 760 000 porównań z 20 000 losowych wejść i o 197 kB mniejszym bundlem.
Czego się nauczyłem
- Mierzyć, nie zgadywać: zmiana reguły pokrycia weszła dopiero po statystyce na tysiącach realnych godzin, a nie po jednym zrzucie ekranu.
- Czysta logika poza pętlą klatek i jedno źródło stałych dla TS i GLSL to warunek, żeby dało się rozwijać shader bez strachu.
- Premultiplied alpha z sRGB daje białe obwódki, uniformy float32 rosną w czasie, a renderer.info resetuje się w useFrame. Każde z tych kosztowało wieczór.