CASE STUDIESProjekt własny
Polonez Autodrive: ewolucja interaktywnej kolorowanki 3D
Synthwave’owa kolorowanka z mini-grą miała w pierwszej wersji 16 błędów klasy P0, trzy niezależne zegary i sterowanie zależne od odświeżania monitora. Analiza pierwszej wersji, przeniesienie projektu z czystego JS i HTML na Vite z nowym szkieletem oraz proceduralna scena w miejsce plików FBX.
- Kiedy
- 2024–2026
- Rola
- Autor: modele 3D, kod, UI, wdrożenie
- Stack
- Three.jsVanilla JSGLSL3Ds MaxViteVitest
- Linki
- repozytoriumdemo

16
błędów P0 znalezionych w wersji 1
121
testów jednostkowych w wersji 2
−29%
draw calli na klatkę po ograniczeniu cieni
Problem
Pierwsza wersja z 2024 roku działała, ale źle się starzała: Three.js r116, czysty JavaScript bez bundlera, GUI na 600 linii kopiowanego kodu. Analiza pierwszej wersji, którą zrobiłem w październiku 2026, wypisała 16 błędów klasy P0. Trzy niezależne zegary (pętla gry, osobny requestAnimationFrame na obiekt i setTimeout) sprawiały, że na monitorze 144 Hz auto sterowało się 2,4 raza szybciej niż na 60 Hz. Pasek ładowania był fikcją z timerów od 2 do 8 sekund i przy gzipie pokazywał „Infinity%”. Do tego wycieki GPU bez dispose, hitbox większy od modelu, wireframe jako osobny klon synchronizowany ręcznie i ten sam plik FBX ładowany dwa razy.
Drugi problem pojawił się przy aktualizacji bibliotek: nowy FBXLoader triangulował niepłaskie czworokąty wzgórz algorytmem earcut i gubił 37 z 252 trójkątów. Modele z 3ds Max wracały z kamerami w hierarchii i kołami o osi poza środkiem, przez co opona biła przy obrocie.
Podejście
Zamiast poprawiać stary kod w miejscu, przeniosłem projekt z czystego JS i HTML na Vite i nowy szkielet: skoro aktualizacja bibliotek zmieniała każdy plik, stary kod był więcej wart jako specyfikacja niż jako baza. Oznaczyłem wersję 1 tagiem, zrobiłem zrzuty referencyjne i w siedmiu fazach doszedłem do parytetu funkcji na Three.js r186 z Vite, a potem dokładałem nowe rzeczy.
Szkielet wersji 2 to jedna pętla na THREE.Timer z obcięciem delty do 0,1 s i podpięciem pod document, żeby ukryta karta nie skakała po powrocie; czysta maszyna stanów gry (idle, free, countdown, playing, gameover); ładowanie przez LoadingManager i Promise.all z prawdziwym postępem; jeden plik konfiguracji jako źródło palety, warstw, parametrów rozgrywki i tekstów.
Rozwiązanie
- Własny FanFBXLoader podmienia triangulację na wachlarz, tak jak robił to r116, więc wzgórza wracają w komplecie. Funkcja czyszcząca usuwa kamery z eksportu, a koła są centrowane na osi po wczytaniu.
- Scena proceduralna zamiast plików: seedowany szum simplex z ridged fBm, siatka wysokości dzielona wzdłuż przekątnej o mniejszej różnicy wysokości (grzbiety zamiast wzoru), chunki wzgórz jako taśmociąg wyrastający przed autem i zapadający się za nim, pasmo gór rozstąpione przed słońcem na drogę, miasto ze 175 wież z własną mgłą.
- Kolory w czasie rzeczywistym: lil-gui budowane w pętli z konfiguracji warstw, losowe palety ze schematów barw (komplementarne, triada, split, analogiczne) z pilnowanym kontrastem linii do wypełnienia co najmniej 3:1, przejście tweenem w sekundę, a HUD i panel przejmują odcień sceny.
- Wireframe liczony w shaderze z atrybutem odległości do krawędzi: szerokość w jednostkach świata i zanik małych trójkątów, bo linie GL mają 1 piksel i palmy w oddali zlewały się w plamę. Selektywny bloom na osobnej warstwie.
- Mini-gra: hitbox policzony raz na model, bounds auta raz na klatkę, pula sklonowanych materiałów do fade’ów, bo dispose przy spawnie dawał 40–70 ms przycięcia.
- Dotyk: przyciski ekranowe z setPointerCapture, minimalne 70° FOV w poziomie w pionowym kadrze i kamera przesuwająca się bokiem za autem.
Efekt
Wersja 2 to około 5,6 tys. linii JavaScriptu z typowaniem w JSDoc, 121 testów jednostkowych w 23 plikach i CI, które przy każdym pushu sprawdza typy, testy i build. Cienie ograniczone do otoczenia auta zdjęły 18 procent trójkątów i 29 procent draw calli na klatkę. Z repozytorium zniknęło około 1,9 MB martwych plików, w tym 820 kB PNG słońca i siatki, które teraz liczą shadery.
Czego się nauczyłem
- Jedna pętla i jedno źródło danych usuwają całe klasy błędów, nie pojedyncze bugi.
- Przeniesienie projektu na nowy szkielet bywa tańsze niż poprawianie go w miejscu, gdy aktualizacja i tak dotyka każdego pliku. Warunek: stary kod zostaje jako specyfikacja, z tagiem i zrzutami referencyjnymi.