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
Yet Another Weather App: scena 3D z chmurami nad mapą
  • 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.

© 2026 SKOFTWARE Maciej Skorus

Polityka Cookies