Witam na moich kursach OpenGL. Jestem przeciętnym gościem z pasją OpenGL. Pierwszy raz usłyszałem o OpenGL kiedy 3Dfx wydało swoje pierwsze akceleratory OpenGL dla Voodoo1. Poczułem, że OpenGL jest czymś, czego muszę się nauczyć. Niestety, nie mogłem znaleźć żadnych informacji w książkach i necie. Spędziłem wiele godzin aby sprawić żeby kod zaczął działać. Mailowałem i Ircowałem szukając pomocy. Ludzie znający OpenGL uważali się za elitę i nie chcieli dzielić się informacjami. To było bardzo frustrujące.
Stworzyłem tą stronę dla ludzi zainteresowanych OpenGL'em, aby mogli tu przyjść i zaczerpnąć pomocy, jeśli będą jej potrzebować. W każdym swoim tutorialu staram się wyjaśniać wszystko tak dokładnie jak to tylko możliwe. Staram się aby mój kod był prosty (żadnego MFC!) Kompletny nowicjusz w Visual C++ i OpenGL będzie w stanie zrozumieć ten kod. Jest to strona jakich wiele. Jeśli jesteś hardcorowym programistą OpenGL, to może ona być dla ciebie za prosta.
Ten tutorial został napisany na nowo w styczniu 2000. Nauczy cię otwierać puste okno GL w pełnym ekranie lub okienku. Kod jest bardzo elsatyczny, więc bedziesz mógł go używać w swoich projektach. Wszystkie moje kursy są bazowane na tym właśnie kodzie!. Stworzyłem kod elastyczny i potężny zarazem. Wszystkie błędy zostały wykryte. Nie ma wycieków pamięci, kod jest łatwy w modyfikacji. Podziękowania dla Fredryka Echolsa za modyfikacje.
Zacznę ten kurs od wskoczenia w kod. Pierwszą rzeczą do zrobienia jest projekt w Visual C++. Jeśli nie umiesz go utworzyć, to nie powinieneś się uczyć OpenGL (czyt. Wynocha). Kilka wersji Visual C++ wymaga zmiany "bool" na BOOL, true na TRUE i false na FALSE. Kod był kompilowany na VC 4, VC 5 i VC 6 bez problemu.
Kiedy stworzysz nowy projekt Win32 (NIE konsola) w VC++, zalinkuj biblioteki OpenGL. W VC przejdź do Project->Settings->LINK. Pod "objekt/Library Modules" na początku linii (przed kernel32.lib) dopisz: opengl32.lib, glu32.lib i galux.lib. Kiedy to zrobisz, kliknij OK i jesteś gotowy do pisania :)
UWAGA #1: Kilka kompilatorów nie ma zdefiniowanego CDS_FULLSCREEN, jeśli otrzymasz error o tym, dodaj gdzieś na początku programu: #define CDS_FULLSCREEN 4.
UWAGA #2: Kilka kompilatorów nie posiada biblioteki GLAUX. Dograj ją sobie z działu download.
Pierwsze 4 linie kodu to dołączanie bibliotek, których używamy. Spójrz:
Następnie musisz stworzyć wszystkie zmienne które zamierzasz użyć w programie. Program będzie tworzyć czyste okno OpenGL, więcc wiele tych zmiennych nie będzie ;) Te zmienne są bardzo ważne, ponieważ każdy program OpenGL ich używa.
Najpierw ustawiamy Kontekst Renderowania. Każdy program OpenGL jest połączony z kontekstem renderowania. To jest takie coś, co łączy polecenia OpenGL z kontekstem urządzenia. Kontekst renderowania jest zdefniowany jako hRC ((handle) Rendering Context). Aby rysować po oknie twój program potrzebuje kontekstu urządzenia (druga linia). Ten kontekst zdefiniowany jest jako hDC (Device Context). hDC łączy okno z GDI (interfejs graficzny). hRC łączy OpenGL z hDC.
Trzecia linia to zmienna hWnd - uchwyt okna podpięty pod nasze okno. Ostatnia tworzy instancję naszego programu.
Dalej. Pierwsza linia to tablica która będzie prechowywać stan wciśniętych klawiszy. Jest wiele sposobów na pobranie stanu klawiatury, ale ten sposób wybrałem ja. Jest dobry, bo może przechowywać więcej klawiszy niż jeden.
Zmienna active będzie używana do sprawdzania czy program nie jest zminimalizowany.
Zmienna fullscreen mówi sama za siebie. Jeśli będzie true, ekran będzie w trybie pełnoekranowym. Jeśli zaś false, będzie on w trybie okienkowym. Ważne jest aby ta zmienna była widziana przez wszystkie funkcje w programie.
Teraz musimy zadeklarować WndProc(). Powodem jest to, że CreateGLWindow() ma odwołanie do WndProc() a, WndProc przychodzi po kodzie CreateWindow(). Robimy to ze względu na język C.
Zadaniem następnej sekcji kodu jest zmiana rozmiaru sceny OpenGL, wtedy gdy okno zmieni rozmiar (w trybie okienkowym). Kiedy rozciągniesz okno, będzie ustawiać perspektywę do rozmiarów okna.
Następne linie ustawiają ekran dla widoku perspektywicznego. Znaczy to, że to co jest dalej, jest mniejsze. Stworzy to realistyczniejszą scenę. Perspektywa obliczana jest z 45 stopniowym widokiem bazowanym na rozmiarze okna. 0.1f i 100.0f są to początkowe i końcowe punkty zasięgu widzenia.
glMatrixMode(GL_PROJECTION) oznacza, że następne linie będą oddziaływać na macierz projekcji. Macierz projekcji jest potrzebna by dodać perspektywę do naszej sceny. glLoadIdentity() resetuje macierz do pierwotnego stanu. Po glMatrixMode(GL_MODELVIEW) oznacza że następne transformacje będą odziaływać na macierz modeli. Ta macierz przechowuje informacje o objekcie. Na koniec resetujemy macierz modeli. Nie martw się, jeśli nie rozmiesz tego. Wyjaśnię to w następnych kursach. Wystarczy tylko że wiesz, że to jest potrzebne do perspektywy na scenie.
W następnej sekcji kodu ustawimy OpenGL. Ustawimy kolor czyszczenia ekranu, włączymy bufor głębokości i gładkie cieniowanie modeli, itp. Ta rutyna nie będzie włączana, póki okno OpenGL się nie stworzy.
Następna linia właczna gładkie cieniowanie. Gładkie cieniowanie ładnie miesza kolory na wielkącie i wygładza światło. Wyjaśnię to w innym kursie.
Następne linie ustawiają kolor czyszczacy ekran. Jeśli nie wiesz jak działają kolory, szybko to wyjaśnię. Wartości kolorów są od 0.0 do 1.0. 0.0 oznacza całkowitą ciemność, a 1.0 najjaśniejszy. Pierwszy parametr po glClearColor to intensywność czerwonego, drugi zielonego, trzeci niebieskiego. Ostatni parametr to wartość alpha. Kiedy chodzi o czyszczenie ekranu nie martw się o 4 parametr.
Stworzysz różne kolory poprzez mixowanie trzech podstawowych kolorów (czerwony, zielony, niebiski). Mam nadzieję, że tego sie nauczyłeś w szkole. Więc glClearColor(0.0, 0.0, 1.0, 0.0) oznacza kolor jasny niebiski.
Następne trzy linie robią bufor głebi. Myśl o buforze głębi jak warstwach na ekranie. Ten bufor ustala jak głęboko jest objekt, czyli co ma być rysowane jako pierwsze. Bufor Głębi to bardzo ważna część OpenGL.
Teraz powiemy OpenGL, że chcemy najlepszą perspektywę. Obniży to szybkość działania programu, ale będzie ładniej.
Nareszcie zwracamy TRUE. Przy inicjalizacji sprawdzamy co zwróciła funkcja. Jeśli zwróci false, oznaczać to będzie, że wystąpił błąd.
Ta sekcja służy do rysowania. Wszystko co zostanie narysowane i wyświetlone jest właśnie tutaj. Każdy kurs doda kod właśnie tu. Jeśli już coś umiesz w OpenGL, możesz spróbować stworzyć kształty przed glLoadIdentity i przed retrun true. Jeśli jesteś nowicjuszem, poczekaj na mój następnu kurs. Na razie tylko czyścimi ekran na ustawiony wcześniej kolor. return TRUE mówi programowi, że nie wystąpiły problemy. Jeśli chcesz zatrzymać program, daj return FALSE gdzieś przed return TRUE, lub powiedz programowi że wystąpił problem ;)
Następna część kodu jest uruchamiana przed zakończeniem programu. Zadaniem KillGLWindow() jest zwolnienie pamięci z Kontekstu renderowania, kontekstu urządzeniowego i uchwyt okna. Dodałem dużo sprawdzania błędów. Jeśli program nie będzie mógł zwolnić czegoś, zostanie pokazane okienko z błędem.
Pierwszą rzeczą do zrobienia w tej funkcji jest sprawdzenie czy program jest pełnym ekranie. Jeśli tak, to trzeba przełączyć spowrotem. Powininiśmy zniszczyć okno przed wyłączeniem pełnego ekranu, ale niektóre karty tego nie obsługją.
Użyjemy ChangeDisplaySettings(NULL, 0) do przywrócenia orginalnej rozdzielczości. Parametry oznaczają, że wartości są podane w rejestrze Windowsa (rozdzielczość, głębia, częstotliwośc).
Następny kod sprawdza czy mamy kontekst rendowania (hRC). Jeśli nie, program pominie ten krok.
Jeśli mamy kontekst renderowania, kod sprawdzi czy jesteśmy w stanie zwolnić go (nie myl hRC z hDC). Zwróć uwagę na sposób sprawdzania błędów. Normalnie każę programowi spróbować zwolnić to (z wglMakeCurrent(NULL, NULL), następnie sprawdzam czy się udało.
Jeśli nie uda się zwolnić kontekstów, wyskoczy MessageBox() z wiadmością o błędzie. NULL mówi, że okienko nie ma okna pierwotnego. Następny parametr to treść wiadomości. Kolejny, "BŁĄD ZAMYKANIA" to tytuł wiadomości. Ostatnie MB_OK oznacza że ma być jeden guzik - OK. MB_ICONINFORMATION oznacza, że okienko ma mieć ikonkę "i" w chmurce. MB_ICONERROR oznacza czerwone kółko z białym krzyżykiem. Jeśli chcesz się dowiedzieć więcej o okienkach, ucz się WinAPI.
Teraz spróbujemy usunąć (nie mylić z "zwolnić"!) kontekst renderowania. Jeśli się nie uda, pokaże się okienko z errorem.
Jeśli się nie uda pokaże się okienko z wiadomością o błędzie. W każdym wypadku ustawimy hRC na NULL.
Teraz sprawdzimy czy program ma kontekst urządzenia. Jeśli ma, to go zwolnimy. Jeśli się nie uda zwolnić to sami wiecie - okienko z wiadomością i ustawiamy na NULL.<rb>
Teraz sprawdzimy czy jest stworzony uchwyt okna. Jeśli jest, to go zniszczymy używając funkcji DestroyWindow(hWnd). Jeśli się nie uda, pokaże się okienko z errorem i ustawimy hWnd na NULL.
Ustatnią rzeczą do zwolnienia jest klasa okna. Pozwoli nam to poprawnie zamknąć okno.
Nastepna funkcja stworzy okno OpenGL. Spędziłem wiele czasu rozmyślając, czy stworzyć tylko tryb pełnoekranowy który nie wymaga wiele kodu, czy przyjazne okienko, które wymaga dość sporo kodu. Zadecydowałem, że okienko będzie najlepszym wyborem. Wiele osób pytało mnie o to jak zmienić tytuł okienka, jak zmienić format piksela w oknie. Ten kod to robi.
Jak widzisz procedura zwraca bool (true lub false). Pobiera 5 parametrów: tytuł okna, jego szerokość, jego wysokość, głębię kolorów (16/24/32) i tryb pełnoekranowy. Funckja zwróci true jeśli wszystko pójdzie sprawnie.
Teraz zapytamy Windowsa o format piksela takiego jaki my chcemy. Tryb który zwróci Windows będzie przechowywany w zmiennej PixelFormat.
Dalej. wc będzie przetrzymywać naszą Klasę okna (nie mylić z pojęciem klasy z C++). Ta klasa przechowuje informacje o naszym oknie, poprzez zmiane jej niektórych pól, możemy zmienić wygląd okna ;) Każde okno musi mieć klasę okna, więc przed stworzeniem okna MUSISZ zarejestrować klase okna.
dwExStyle i dwStyle przechowywać będą rozszerzony i normalny styl okna. Rozszerzony pozwala nam na takie bajery jak ta wąska ramka wokół okna, guziki, i takie tam. Normalny w sam raz pasuje do trybu pełno ekranowego ;)
Następne 5 lini kodu pobiera górny-lewy i dolny-prawy róg prostokąta. Użyjemy tych wartości do określenia obszaru rysowania na oknie i dopasowania do rozdzielczości. Jeśli stworzymy okno 640 x 480, ramka zabierze trochę rozdzielczości.
Ta linia kodu pobierze wartość z parametru funkcji i da ją do naszej zmiennej globalnej.
W nastepnej porcji kodu pobierzemy instancję okna, a następnie zadeklarujemy klasę okna.
Styl CS_HREDRAW i CS_VREDRAW sprawia, że okno będzie odmalowywane przy zmianie rozmiarów. CS_OWNDC stworzy prywatny kontekst urządzenia dla okna. Kontkekst ten nie jest wspólny dla wszysytkich aplikacji. WndProc jest funkcją która wyczekuje na komunikaty od naszego okna (np. wciśnięcie klawiszy jest komunikatem). Nie używamy żadnych dodatkowych informacji okna, więc dajemy 0. Nastepnie ustawiamy instancję, później ikone na NULL, ponieważ nie chcemy ikonki. Kursor myszy będzie zwyczajną strzałką. Tło nie ma znaczenia bo ekran czyścimy przecież w OpenGL. Nie tworzymy też menu, więc dajmy NULL. Nazwa klasy jest obojętna. Ja dałem "OpenGL" aby było prościej :P
Teraz możemy spokojnie zarejstrować powyższą klasę okna. Jeśli się nie uda, pokaże się okienko z błędem, a funkcja zwróci false.
Teraz sprawdzimy czy program chce być w trybie pełnoekranowym. Jeśli tak to spróbujemy go ustawić.
Teraz ta część kodu z którą wiele osób ma kłopoty. Zmiana trybu na pełny ekran. Jest parę ważnych reguł, których trzeba przestrzegać przy zmianie na pełny ekran. Należy sprawdzić czy wymagana rozdzielczość jest dostępna w pełnym ekranie (np. 800x600) no i najważniejsze: Należy ustawić tryb pełno ekranowy PRZED stworzeniem okna. W tym kodzie nie musisz obawiać się o rozdzielczość, kod jest kontrolowany.
Teraz ważny moment. Ustawiamy ustalony tryb video: Wysokość, Szerokość i głębie kolorów. Wszystkie te informacje są w strukturze dmScreenSettings. ChangeDisplaySettings próbuje zmienić tryb do tego który my chcemy. Używam CDS_FULLSCREEN ponieważ pozwala on nam usunąć pasek start na spodzie ekranu no i nie pozmieniają się rozmiary okienek na ekranie po zakończeniu programu (jeśli nie wiesz o co chodzi daj tam 0, włącz program, i wyłącz).
Jeśli tryb nie jest obsługiwany przez twoją kartę graficzną, pokaże się okienko z dwiema opcjami: Uruchomić w trybie okna, lub zakończyć program.
Jeśli użytkownik zdecyduje się na tryb okienkowy, ustawimy fullscreen na false i program będzie się kontynuował.
Jeśli jednak człowiek zdecydował się na zakończenie, pokaże się okienko o zakończeniu i funkcja zwróci false mówiąc tym samym, że okno nie zostało utworzone. Co za tym idzie program się zamknie.
Ponieważ wszystko mogło pójść dobrze, sprawdzamy jeszcze raz stan fullscreen.
Jeśli jesteśmy w tym trybie, ustawimy styl rozszerzony na WS_EX_APPWINDOW, który ustawi okno do pasku zadań, sprawiając, że okno będzie niewidoczne. Stylu okna damy na WS_POPUP, czyli puste okno, bez guzików, bez ramek. W sam raz dla pełnego ekranu.<rb>
Na koniec sprawimy że kursor myszy zniknie. Jeśli program nie jest interaktywny, miło jest usunąć kursor myszy.
Jeśli używamy okna zamiast pełnego ekranu, dodamy WS_EX_WINDOWEDGE do naszego rozszerzonego stylu okna. Da to bardziej profesjonalny wygląd okna. Dla stylu normalnego damy WS_OVERLAPPEDWINDOW zamiast WS_POPUP. Stworzy to okno z paskiem tytułu, guzikami systemowymi (minializacja/maxymalizacja), ramką i ikonką.
Linia poniżej dopasuje okno do naszego stylu. Dopasowanie stworzy okno w rozdzielczości takiej jaką dokładnie chcemy. Normalnie ramki powiększają okno, ale używając AdjustWindowRectEx przykryją one kawałeczek sceny OpenGL. W trybie pełno ekranowym ta funkcja nic nie robi.
Teraz stworzymy nasze okno i sprawdzimy czy stworzyło się dobrze. Użyjemy CreateWindowEx(). Używamy rozszerzonego stylu okna. Nazwa klasy musi być ta sama co w strukturze ("OpenGL"). Tytuł okna, Styl okna, Pozycja rogu okna (0,0 jest najbezpieczniejsza), szerokość i wysokość okna. Nie chcemy okna potomnego, nie chcemy też menu, więc dajemy te parametry na NULL. Podajemy instancje, i NULL na ostatni parametr.
Zauważ, że użyliśmy styli WS_CLIPSIBLINGS i WS_CLIPCHILDREN razem ze stylami które wcześniej użyliśmy. WS_CLIPSIBLINGS i WS_CLIPCHILDREN są wymagane do poprawnego działania OpenGL.
Teraz sprawdzimy czy okno się stworzyło poprawnie. Jeśli się stworzyło, hWnd będzie przetrzymywać uchwyt okna. Jeśli nie, pokaże się komunikat o błędzie i program się zakończy.
Następujący kod opisuje format piksela. Wybierzemy format który jest obsługiwany przez OpenGL, podwójne buforowanie, razem z RGBA( czerwony, zielony, niebieski i alpha). Spróbujemy znaleźć taki format piksela który pasuje do wybranej głębi kolorów (16, 24, 32). Na koniec utworzymy 16 bitowy bufor Z. Pozostałe parametry nie są używane lub nie są ważne.
Jeśli nie wystąpiiły błędy podczas tworzenia okna, spróbujemy pobrać kontekst urządzenia. Jeśli się nie uda, pokaże się tradycyjnie okienko z errorem ;) i program się zakończy.
Jeśli udało się pobrać kontekst urządzenia dla naszego okna, spróbujemy znaleźć format piksela zgodny z tym który opisaliśmy wcześniej. Jeśli Windows nie znajdzie tego formatu, pokaże się sami wiecie co i program się zakończy.
Jeśli się znajdzie taki format, spróbujemy go ustawić. Jeśli się nie da, pokaże się wiadomość o błędzie i program się zakończy.
Jeśli format ustawi się pomyślnie, spróbujemy pobrać kontekst renderowania. Jeśli się nie uda, zostanie wyświetlona wiadomość o błędzie.
Jeśli nie wystąpią błędy i stworzyliśmy oba konteksty, pozostało uaktywnić kontekst Renderowania. Jeśli się nie uda pokaże się stosowna wiadomość.
Jeśli wszystko poszło sprawnie i okno OpenGL zostało stworzone, możemy je pokazać i ustawić na przód dając większy priorytet. Następnie wywołamy ReSizeGLScene() podając szerokość i wysokość ekranu aby ustawić perspektywę.
Na koniec wskakujemy w InitGL() gdzie możemy ustawić światła, tekstury, i wszystko z tym związane. Możesz zrobić własną kontrolę błędów w InitGL() i zwrócić true lub false. Na przykład podczas ładowania tekstur nie zostanie odnaleziona któraś, to pokaże się error ;)
No i pozostaje koniec funkcji. Jeśli wcześniej nie wystąpiły błędy, zwróci ona true do WinMain(), kontynuując program.
Teraz stworzymy funkcję obsługującą komunikaty okna zarejestrowaną w klasie okna.
Teraz sprawdzimy jakiego typu jest wiadomość otrzymywana (uMsg).
jeśli tym komunikatem okaże się WM_ACTIVATE, sprawdzimy czy okno jest wciąż aktywne. Jeśli zostało zminimalizowane, ustawimy active na false. Jeśli jest aktywne,damy active na true.
Jeśli tą wiadomością jest WM_SYSCOMMAND (komenda systemowa), sprawdzimy wParam. Jeśli jest nim SC_SCREENSAVE lub SC_MONITORPOWER zwócimy zero zapobiegając pojawieniu się zgaszacza ekranu.
Jeśli wiadomość to WM_CLOSE, okno zostanie zamknięte. Wyślemy wiadomość zamykania, pętla główna zotanie przerwana. Zmienną done ustawimy na true, więc pętla się przerwie i program się zamknie.
Jeśli klawisz został wciśnięty, możemy określić jaki on jest za pomocą wParam. Wtedy zapisujemy w odpowiednim polu tablicy keys[] true. Później możemy odczytać z tej tablicy jakie klawisze są wciśnięte ;) pozwala nam to sprawdzić wszystkie klawisze za jednym zamachem.
Jeśli jakiś klawisz został odciśnięty, znajdujemy go za pomocą wParam. Następnie ustawiamy odpowiednie pole w tablicy keys[] na false. Każdy klawisz na klawiaturze reprezentowany jest przez liczbę od 0 - 255. Kiedy wciśniesz klawisz o numerze 40, pole tablicy keys[40] ustawione zostanie na true. Jeśli go odciśniesz, pole zostanie ustawione na false.
Jeśli komendą jest zmiana rozmiaru okna, to jego nowa szerokość jest zapisana w lParam, w jego dwóch częściach: LOWORD i HIWORD. Po zmianie rozmiaru okna musimy przestawić perspektywe do nowych rozmiarów korzystając z naszej funkcji ReSizeGLScene(). Scena OpenGL będzie na nowo dopasowana do nowych wymiarów okna.
To już wszystkie wiadomości na których nam zależy. Reszta będzie obsługiwana przez system Microsoft Windows.
Teraz czas na najważnieszą część programu: Funkcja WinMain().
Ustawimy dwie zmienne. msg będziemy używać do sprawdzania czy są jakieś komunikaty okna. done użyjemy do sprawdzania czy program jeszcze ma działać. Jeśli done ustawimy na true program zostanie zakończony.
Ta część kodu jest opcjonalna. Pokazuje ona okienko pytające, czy chcemy aby program uruchomił się w pełnym ekranie, czy w trybie okienkowym.
Teraz wywołamy naszą funkcję tworzącą okno OpenGL. Podamy tytuł, szerokość i wysokość, głebie koloru i true lub false (pełny ekran). Jestem bardzo szczęśliwy, że ten kod jest taki prosty.
Teraz zaczniemy pętle główną programu. Będzie ona tak długo lecieć, póki done będzie ustawione na false.
Pierwszą rzeczą w takiej pętli jest sprawdzenie czy jakieś komunikaty okna czekają na obsługę. Sprawdzimy to przy pomocy PeekMessage().
W tej częsci kodu sprawdzimy czy komunikatem nie jest WM_QUIT spowodowany przez PostQuitMessage(0). Jeśli nim jest, ustawimy done na true i będzie koniec programu.
Jeśli komunikat jest inny, to obsłuży go funkcja WndProc() lub Windows.
Jeśl nie ma żadnych komunikatów, możemy śmiało rysować naszą scene OpenGL. Pierwsza linia kodu sprawdzi czy program jest aktywny. Jeśli wciśnięto ESC, ustawimy done na true, przerywając tym samym pętle.
Jeśli program jest aktywny i Esc nie jest wciśnięty, możemy renderować scene i zamienić bufory (poprzez użycie podwójnego buforowania, aby uzyskać płynną animację). Dzięki podwójnemu buforowaniu wszystko jest rysowane na niewidzialnym ekranie (drugim buforze), a na koniec przerzucone na ten prawdziwy ;)
Następna porcja kodu jest nowa. Pozwala ona przełączyć za pomocą klawisza F1 ekran z trybu pełnoekranowego na okienkowy i na odwrót :)
Jeśli zmienna done nie jest ustawiona na false, program się zakończy. Zamkniemy okno OpenGL poprawnie.
W tym kursie wyjaśniłem to bardzo dokładnie, każdy krok został opisany: włączanie trybu pełnoekranowego, tworzenie okna, uruchamianie OpenGL i zamykanie okna. Spędziłem 2 tygodnie pisząc ten kod, ciągle poprawiałem drobne błędy. Dziś możesz go ściągnąć i wykorzystać w swoich projektach. Jeśli gdzieś popełniłem błąd, powiadom mnie.