Moja nauka WebGL (2)

Przepisałem na nowo swój silnik 3D. Już dwa tygodnie temu i teraz rozwijam. I trochę zmieniam zdanie w kwestii tego, co jest najtrudniejsze. Napisałem wcześniej:

(najtrudniejsze są chyba przeliczenia matematyczne. No i optymalizacja. Nie zawsze wiem, dlaczego mam spadek FPSów na przykład).

Teraz myślę tak - tych obliczeń matematycznych wcale nie ma tak dużo (przynajmniej do tej pory). Optymalizacja też jest dość prosta (przynajmniej do tej pory udało mi się zrobić jakąś tam podstawową optymalizację - np. instanced rendering, trzymanie w pamięci raz utworzonych buforów i listy obiektów do renderingu (zamiast tworzyć to co klatkę) itp.

Jednak silnik się powoli rozrasta i zaczynają mieć znaczenie decyzje projektowe - jak coś zrobić nie od strony technicznej, tylko od strony architektury. Tak, żeby silnik był elastyczny, ale jednak prosty, i nie przeinżynierowany. Abstrakcje są potrzebne, ale trzeba wyczuć, jakie to abstrakcje.

Wydawać by się mogło, że pierwszą rzeczą, jaką należałoby zrobić pisząc coś większego w WebGL jest ubrać API WebGL w wygodne wrappery. I po części tak zrobiłem. Np. mam klasę Framebuffer, która nawiasem mówiąc ma tylko konstruktor. W zasadzie to fabryka, a nie klasa. Tym niemniej pozwala mi tworzyć nowe "bufory ramki" w jednej linijce kodu zamiast w kilkunastu. Również mam klasę Shader, która ułatwia tworzenie shaderów (i która również zawiera sam konstruktor, więc jest taką fabryką, równie dobrze mogłaby to być funkcja, która zwraca jakiś obiekt). Czyli opakowuję niewygodne API WebGLa w funkcje-fabryki.

Jednak nie do wszystkiego mam wrappery. Kod odpowiedzialny bezpośrednio za rendering robię w dość prosty sposób - mam funkcje typu renderMesh, renderMeshInstanced, prepareToRenderMesh itp. I do tych funkcji podaje parametry, co ma renderować, jakim shaderem itp. I w środku tych funkcji wywołuję bezpośrednio kod WebGL. Bo mógłbym to w inny sposób rozwiązać i np. sprawić, żeby to obiekt Mesh miał metodę render i sam się renderował. Mógłbym też zrobić jakiś wrapper do wygodniejszego ustawiania uniformów (bo na razie wywołuje po prostu np. gl.uniformMatrix4fv czy inne metody kontekstu WebGL). Ale... po co? W sensie na razie jest dobrze jak jest, a nie chcę przeinżynierować. Chociaż akurat do api WebGL to mam pewien pomysł na wrapper, ale w celach optymalizacyjnych (więc będę potrzebował sprawdzić, czy to ma sens w ogóle. Ale o tym już innym razem).

Nie znaczy, że nie ma abstrakcji, ale są to już bardziej moje abstrakcje (wliczając w to ogólne koncepcje z programowania 3D obecne również w innych bibliotekach). Mam np. koncepcję "pass" (do postprocessingu, w Three.js też tak jest - że można coś wyrenderować do bufora poza ekranem, a potem nakładać np. blur). Mam koncepcję drzewa sceny - ale uwaga - nie renderuję bezpośrednio drzewa sceny, tylko przechodzę przez drzewko i zbieram obiekty, które korzystają z tej samej siatki i które mogą być wyrenderowane razem (tzw. instanced rendering). Dzięki temu jest szybciej. Więc mam abstrakcję RenderList, która enkapsuluje listę obiektów do renderowania + trzyma w pamięci bufory z danymi tych obiektów.

Mam również koncepcję "widoku"(View), która reprezentuje "komplet rzeczy do renderowania" tj. widok ma przypisaną konkretną scenę, kamerę i obszar canvasa, na którym ma się wyrenderować (pozycja x,y, szerokość, wysokość). I mogę tworzyć różne widoki - np. jeden widok może się wyrenderować na górze canvasa, drugi na dole. Mogą mieć różne sceny albo mogą być dwa widoki, które mają tę samą scenę (ale np. z innej pozycji kamery). I ogólnie elastyczność.

Przy czym widok to (obecnie) nie jest żadna klasa. Po prostu koncepcja. Sposób w jaki zapisuję dane w aplikacji i sposób intepretacji tych danych przez mój silnik. Np. tak wygląda kod deklarujący widok w aplikacji korzystającej z mojego silnika:

Jak widzicie, po prostu obiekty JSowe, które mają jakieś swoje właściwości. Ogólnie lubię taki sposób pisania, taki data driven, że masz różne parametry na wejściu, jak coś ma wyglądać, co ma coś robić itp. i przechodzić przez te dane i potem zgodnie z tymi parametrami coś robić (np. renderować). Pozwala to m.in. na decoupling interfejsu od implementacji. No bo jeśli coś jest tylko parametrami, to równie dobrze mogę zrobić np. drugi renderer, który zamiast w WebGL będzie pisany w WebGPU (nowa eksperymentalna technologia, która może w przyszłości zastąpić WebGL). Te same dane, po prostu opis, co gdzie ma być, a sposób interpretacji tych danych czy implementacji renderingu na ich podstawie - to już inna bajka. Podobnie łatwo będzie w przyszłości zserializować takie dane.

No więc ogólnie, żeby podsumować - będzie silniczek <3 na początku robię to na potrzeby gry, a potem zobaczymy. Czekajcie i obserwujcie. Chociaż jeszcze nie opublikowałem tego nigdzie. Ale warto czekać.

Komentarze

Popularne posty z tego bloga

Dlaczego nie da się nadgonić frontendu

Absurdy Rekrutacji 2023

Przygody juniora (1)