Serwis Edukacyjny Nauczycieli w I-LO w Tarnowie ![]() Materiały dla uczniów liceum |
Wyjście Spis treści Wstecz Dalej
Autor artykułu: mgr Jerzy Wałaszek |
©2023 mgr Jerzy Wałaszek
|
SPIS TREŚCI |
Podrozdziały |
Raster posiada określoną liczbę kolumn i linii pikseli. Parametry te nazywamy rozdzielczością obrazu (ang. picture resolution). Typowe rozdzielczości obrazu na ekranie monitora to: 800 x 600, 1024 x 768, 1200 x 1024, 1920 x 1080 (rozdzielczość HD), itd.
Pierwszą umiejętnością w świecie grafiki komputerowej jest postawienie piksela o wybranym kolorze w odpowiednim miejscu na obrazie rastrowym. Jeśli zabierzemy się za to programowo na powierzchni graficznej, to zadanie stanie się dosyć skomplikowane, ponieważ musimy uwzględniać różne sposoby kodowania pikseli (o ile chcemy napisać uniwersalny program, który będzie działał w praktycznie każdym środowisku graficznym). Większość współczesnych kart graficznych koduje piksele za pomocą 32 bitów, a barwy składowe zajmują w tym kodzie po 8 bitów. Tak naprawdę twoja funkcja musi najpierw rozpoznać tryb pracy ekranu (paletowy lub RGB) i podjąć odpowiednie do tego trybu działanie.
Uruchom CodeBlocks, utwórz z szablonu nowy projekt sdl2, skopiuj do edytora poniższy program, skompiluj go i uruchom:
C++// Stawianie punktu programowo //---------------------------- #include <SDL.h> #include <iostream> using namespace std; // Funkcja stawia piksel o zadanym kolorze na pozycji x,y obrazu //-------------------------------------------------------------- void Plotxyc(SDL_Surface * s, int x, int y, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { Uint32 color, * addr; // Obliczamy adres piksela addr = (Uint32 *)s->pixels + x + y * s->w; // Obliczamy kod piksela color = (r << s->format->Rshift) & (s->format->Rmask); color |= (g << s->format->Gshift) & (s->format->Gmask); color |= (b << s->format->Bshift) & (s->format->Bmask); color |= (a << s->format->Ashift) & (s->format->Amask); // Umieszczamy piksel na obrazie * addr = color; } // Rozmiar okna const int W_W = 480; const int W_H = 640; int main(int argc, char* args[]) { // Inicjujemy podsystem wideo if(SDL_Init(SDL_INIT_VIDEO)) { cout << "SDL_Init Error: " << SDL_GetError() << endl; return 1; } // Tworzymy okno SDL_Window * w = SDL_CreateWindow("Piksele", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, W_W, W_H, 0); if(!w) { cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl; SDL_Quit(); return 1; } // Powierzchnia graficzna okna SDL_Surface * s = SDL_GetWindowSurface(w); // Sprawdzamy, czy piksele są 32-bitowe. Jeśli nie, kończymy if(s->format->BitsPerPixel != 32) { cout << "Pixels not 32 bits in size" << endl; SDL_DestroyWindow(w); SDL_Quit(); return 2; } // Blokujemy dostęp do powierzchni graficznej innym procesom SDL_LockSurface(s); // Na środku okna rysujemy kilka kolorowych punktów Plotxyc(s,W_W/2 - 5,W_H/2 - 5,255,0,0,255); // Piksel czerwony Plotxyc(s,W_W/2 + 5,W_H/2 - 5,0,255,0,255); // Piksel zielony Plotxyc(s,W_W/2 + 5,W_H/2 + 5,0,0,255,255); // Piksel niebieski Plotxyc(s,W_W/2 - 5,W_H/2 + 5,255,255,255,255); // Piksel biały, wszystkie składowe równe 255 // Odblokowujemy dostęp do powierzchni SDL_UnlockSurface(s); // Uaktualniamy zawartość okna SDL_UpdateWindowSurface(w); // Czekamy 5 sekund SDL_Delay(5000); // Usuwamy okno SDL_DestroyWindow(w); // Zamykamy SDL2 SDL_Quit(); return 0; } |
Opiszemy działanie funkcji Plotxyc(). Reszta programu jest standardowa i nie powinieneś mieć problemu z jej zrozumieniem. Ostatecznie cofnij się do poprzedniego rozdziału.
W funkcji main() sprawdzamy, czy piksele w powierzchni graficznej okna są 32-bitowe. Jeśli nie, to kończymy, ponieważ funkcja Plotxyc() zakłada, że rozmiar piksela jest 32-bitowy.
Plotxyc(...) | Funkcja otrzymuje w parametrach
kolejno: wskaźnik do struktury SDL_Surface
okna współrzędne x i y piksela składowe koloru: r, g i b przezroczystość a. |
Uint32 color, * addr; | Tworzymy dwie zmienne. Zmienna color będzie zawierała kod piksela do wstawienia na powierzchnię graficzną. Zmienna addr jest wskaźnikiem, czyli adresem piksela w pamięci komputera. |
addr = (Uint32 *)s->pixels + x + y * s->w; | Wykorzystując dane zawarte w strukturze
SDL_Surface, obliczamy adres piksela. Pole
pixels w strukturze SDL_Surface przechowuje adres
początku bufora zawierającego piksele obszaru okna. W
języku C++ wskaźniki posiadają typy. Kompilator
wykorzystuje te typy do wyznaczania właściwych
przesunięć. Na przykład, załóżmy, że mamy wskaźnik a,
który wskazuje obiekt typu Uint32. Typ Uint32
oznacza liczbę binarną
Jeśli dodasz do wskaźnika a 1, to nie otrzymasz adresu drugiego bajtu danej Uint32. Operacje na wskaźnikach są zawsze skalowane i dodanie 1 do wskaźnika a ustawi go na adres następnej danej Uint32:
Czyli faktycznie dodanie 1 do wskaźnika a zwiększy jego zwartość o 4, a nie o 1. Wskaźniki należy zatem rozumieć jako adresy określonych obiektów w pamięci. Zwiększanie/zmniejszanie wskaźnika powoduje ustawienie adresu innego obiektu typu, który wskaźnik wskazuje. Musisz to dobrze zrozumieć, aby poradzić sobie z operacjami na wskaźnikach. W naszym programie najpierw dokonujemy rzutowania wskaźnika pixels (który wskazuje obiekty typu void, a więc bez określenia rozmiaru) na adres danej Uint32, ponieważ zakładamy, że piksele w obszarze graficznym są danymi typu Uint32. Gdy wykonane zostanie rzutowanie, kompilator będzie traktował wskaźnik pixels tak, jakby wskazywał on dane Uint32. Do adresu dodajemy przesunięcie x, co da nam adres piksela w pierwszej linii obrazu leżącego na pozycji x w obszarze graficznym. Kolejnym czynnikiem, który dodajemy do wskaźnika, jest przesunięcie y pomnożone przez liczbę pikseli w linii obrazu (czyli szerokość okna). Dzięki tej operacji otrzymamy adres piksela leżącego w linii y na pozycji x. |
color = (r << s->format->Rshift) & (s->format->Rmask); | Ta operacja ustawia w kodzie piksela pole składowej czerwonej. |
color |= (g << s->format->Gshift) & (s->format->Gmask); | Do kodu piksela dołączamy pole składowej zielonej. |
color |= (b << s->format->Bshift) & (s->format->Bmask); | Dołączamy pole składowej niebieskiej. |
color |= (a << s->format->Ashift) & (s->format->Amask); | Dołączamy kanał alfa (tutaj niewykorzystywany, zwykle ustawiony na 255, co oznacza, iż piksel jest nieprzezroczysty). |
* addr = color; | Gdy skompletujemy pełen kod piksela, umieszczamy go w buforze pod wyliczonym adresem |
Jak widzisz, programowe ustawianie pikseli w obszarze okna jest możliwe, lecz niezalecane, ponieważ musisz rozważać różne sposoby kodowania pikseli. Gdy wykonamy to samo zadanie za pomocą akceleratora, operacja stanie się dużo prostsza, ponieważ sprawy techniczne kodowania pikseli pozostawiamy do rozstrzygnięcia sprzętowi karty graficznej.
Poniższy program jest dokładnym odpowiednikiem poprzedniego, lecz wykorzystuje kontekst graficzny do rysowania pikseli. Jest to preferowany sposób tworzenia grafiki w SDL2. W dalszych programach skupimy się zatem na sprzętowym tworzeniu grafiki, ponieważ akcelerator zrobi to setki razy szybciej od procesora.
C++// Stawianie punktu sprzętowe //---------------------------- #include <SDL.h> #include <iostream> using namespace std; // Rozmiar okna const int W_W = 480; const int W_H = 640; int main(int argc, char* args[]) { // Inicjujemy podsystem wideo if(SDL_Init(SDL_INIT_VIDEO)) { cout << "SDL_Init Error: " << SDL_GetError() << endl; return 1; } // Tworzymy okno SDL_Window * w = SDL_CreateWindow("Piksele", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, W_W, W_H, 0); if(!w) { cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl; SDL_Quit(); return 1; } // Tworzymy kontekst graficzny przyspieszany sprzętowo SDL_Renderer * r = SDL_CreateRenderer(w,0,0); if(!r) { cout << "SDL_CreateRender Error: " << SDL_GetError() << endl; SDL_DestroyWindow(w); SDL_Quit(); return 1; } // Na środku okna rysujemy kilka kolorowych punktów SDL_SetRenderDrawColor(r,255,0,0,255); SDL_RenderDrawPoint(r,W_W/2 - 5,W_H/2 - 5); SDL_SetRenderDrawColor(r,0,255,0,255); SDL_RenderDrawPoint(r,W_W/2 + 5,W_H/2 - 5); SDL_SetRenderDrawColor(r,0,0,255,255); SDL_RenderDrawPoint(r,W_W/2 + 5,W_H/2 + 5); SDL_SetRenderDrawColor(r,255,255,255,255); SDL_RenderDrawPoint(r,W_W/2 - 5,W_H/2 + 5); // Uaktualniamy zawartość okna SDL_RenderPresent(r); // Czekamy 5 sekund SDL_Delay(5000); // Usuwamy kontekst graficzny SDL_DestroyRenderer(r); // Usuwamy okno SDL_DestroyWindow(w); // Zamykamy SDL2 SDL_Quit(); return 0; } |
Nowością w tym programie jest funkcja:
SDL_RenderDrawPoint(r,x,y)
r – wskaźnik do struktury SDL_Renderer
x,y – współrzędne piksela
Piksel rysowany jest w kolorze, który został ustawiony przez funkcję SDL_SetRenderColor(), opisaną w poprzednim rozdziale. Jeśli piksel został narysowany na teksturze okna, to funkcja zwraca kod 0. Inny kod świadczy o błędzie. Nasz program nie testuje błędów rysowania pikseli dla prostoty.
Funkcja SDL_Delay() wstrzymuje wykonywanie programu na określoną liczbę milisekund. Jednakże w trakcie wstrzymania nie są obsługiwane w programie żadne zdarzenia. Dlatego twoje okienko pojawia się na ekranie, lecz nic nie możesz z nim zrobić. Nie reaguje na działania myszką, naciśnięcia klawiszy, itp. Zmienimy to.
Podstawą obsługi zdarzeń jest unia SDL_Event.
Dla przypomnienia, unia w języku C++ jest strukturą, która w tym samym bloku pamięci może zawierać dane różnego typu. Aby zrozumieć różnicę pomiędzy strukturą a unią, rozważmy prosty przykład. Tworzymy strukturę, która zawiera 3 pola:
struct struktura { char a; int b; double c; };
W pamięci komputera struktura taka wygląda następująco:
Każde z pól a, b, c jest przechowywane osobno w pamięci i możesz umieszczać w nich jednocześnie różne dane.
Z unią jest nieco inaczej. Jeśli stworzymy unię:
union unia { char a; int b; double c; };
to w pamięci będzie ona wyglądała następująco:
Pola zajmują w pamięci ten sam adres i w danej chwili możesz przechowywać tylko jedną daną. Jeśli do innego pola wprowadzisz inną daną, to nadpisze ona częściowo daną przechowywaną poprzednio. Unia nadaje się do przechowywania różnych danych, lecz w danej chwili może przechowywać tylko jedną z nich.
Uruchom CodeBlocks, utwórz projekt konsoli, przekopiuj do edytora poniższy program, skompiluj i uruchom go:
C++// Struktura i unia #include <iostream> using namespace std; int main() { struct { char a; int b; double c; } struktura; union { char a; int b; double c; } unia; struktura.a = 'A'; struktura.b = 199999; struktura.c = 3.1415; unia.a = 'A'; unia.b = 199999; unia.c = 3.1415; cout << "STRUKTURA:" << endl << "POLE a = " << struktura.a << endl << "POLE b = " << struktura.b << endl << "POLE c = " << struktura.c << endl << endl << "UNIA:" << endl << "POLE a = " << unia.a << endl << "POLE b = " << unia.b << endl << "POLE c = " << unia.c << endl << endl; return 0; } |
Wynik programu jest następujący:
STRUKTURA: POLE a = A POLE b = 199999 POLE c = 3.1415 UNIA: POLE a = o POLE b = -1065151889 POLE c = 3.1415
W programie tworzymy strukturę i unię o takich samych polach. W strukturze i w unii zapisujemy w kolejnych polach te same dane, po czym wyświetlamy zawartość struktury i unii. Jak widzisz, struktura przechowała wszystkie dane tak, jak je wprowadzono, ponieważ pola struktury zajmują w pamięci osobne miejsca. W przypadku unii jedynie dana wprowadzona jako ostatnia zachowała swoją wartość. Poprzednie dane uległy uszkodzeniu, ponieważ pola unii zajmują w pamięci to samo miejsce i zostały nadpisane.
Unia SDL_Event jest unią różnych struktur opisujących rodzaj zdarzenia, które wystąpiło w systemie. Nie będę tutaj opisywał całości, ponieważ zrobię to w osobnym rozdziale.
Unia SDL_Event zawiera dwa elementy: pole type oraz pole będące strukturą zdarzenia:
union SDL_Event { Uint32 type; struktura zdarzenia; };
Pola te nakładają się na siebie. W każdej strukturze zdarzenia jest również pole type, które odpowiada dokładnie polu type unii SDL_Event. W polu tym SDL2 umieszcza stałą, która definiuje rodzaj zdarzenia, a zatem i resztę pól struktury. Każde zdarzenie posiada swoją własną strukturę.
Obsługa zdarzeń w SDL2 wygląda w skrócie tak:
Gdy wystąpi zdarzenie, jest ono umieszczane w kolejce zdarzeń (ang. event queue). Pozostaje tam dotąd, aż twój program odczyta je za pomocą jednej z funkcji:SDL_WaitEvent(e) – czeka aż w kolejce pojawi się
zdarzenie, pobiera je i umieszcza jego strukturę w unii
SDL_Event wskazanej adresem e. W przypadku błędu zwraca
0, inaczej zwraca 1 jako wynik.
SDL_PollEvent(e) – jeśli w
kolejce znajduje się zdarzenie, to zostanie pobrane i jego
struktura umieszczona w unii SDL_Event wskazanej adresem e.
W takim przypadku zwraca wynik 1. Jeśli w kolejce nie ma
zdarzenia, to zwraca 0.
Różnica między tymi funkcjami jest taka, iż SDL_PollEvent() nie czeka na zdarzenie, lecz czyta to, co znajdzie w kolejce. Jeśli nic nie znajdzie, to wraca z wynikiem 0. Pozwala to programowi wykonywać inne operacje między zdarzeniami. Istotne to jest np. przy animacjach.
Gdy unia SDL_Event zostanie wypełniona strukturą zdarzenia, sprawdzasz jej typ i podejmujesz odpowiednie działania, co nazywamy obsługą zdarzenia.
Fragment kodu obsługującego zdarzenia jest następujący:
SDL_Event e; // Unia bool running = true; while(running) { SDL_WaitEvent(&e); // Czekamy na zdarzenie switch(e.type) { case ... // Obsługa zdarzenia } }
lub
SDL_Event e; bool running = true; while(running) { while(SDL_PollEvent(&e)) // Dopóki kolejka zawiera zdarzenia, pobieramy je switch(e.type) { case ... // Obsługa zdarzenia } }
Oba kody są sobie równoważne.
W naszych programach będziemy reagowali na zdarzenie zamknięcia okna. Gdy użytkownik zamknie okno w kolejce zdarzeń pojawi się zdarzenie SDL_QuitEvent. Jego struktura jest następująca:
struct SDL_QuitEvent { Uint32 type; Uint32 timestamp; } quit;
W polu type znajduje się wartość SDL_QUIT. Pole timestamp zawiera liczbę milisekund działania programu. Dostęp do pola timestamp odbywa się poprzez nazwę struktury quit.
Zobaczmy, jak to działa w praktyce.
Uruchom CodeBlocks. Utwórz z szablonu projekt sdl2. Skopiuj do edytora poniższy kod, skompiluj i uruchom go:
C++// Zdarzenie SDL_QUIT #include <SDL.h> #include <iostream> using namespace std; //Rozmiar okienka const int W_W = 640; const int W_H = 480; int main(int argc, char* args[]) { // Okno robocze SDL_Window * w = NULL; // Inicjujemy SDL if(SDL_Init(SDL_INIT_VIDEO)) { cout << "SDL_Init Error: " << SDL_GetError() << endl; return 1; } // Tworzymy okno w = SDL_CreateWindow("Zamknij mnie!!!", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, W_W, W_H, 0); if(!w) { cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl; SDL_Quit(); return 2; } // Czekamy na zdarzenie SDL_QUIT SDL_Event e; while(1) { SDL_WaitEvent(&e); if(e.type == SDL_QUIT) { cout << "Program closed after " << e.quit.timestamp << " miliseconds" << endl << endl; break; } } // Usuwamy okno SDL_DestroyWindow(w); // Kończymy pracę z SDL2 SDL_Quit(); return 0; } |
Program w standardowy sposób tworzy okno. Następnie w pętli czeka aż wystąpi zdarzenie SDL_QUIT. Wtedy wypisuje czas wykonywania się i kończy działanie.
Zwróć uwagę, że teraz okienko można przesuwać po ekranie oraz minimalizować na pasek zadań. To właśnie jest zaletą obsługi zdarzeń.
Gdy mamy już rozwiązany problem zamykania okna w odpowiedzi na działania użytkownika, wróćmy do naszych pikseli.
Pierwszy program wypełnia okno pikselami o przypadkowym kolorze i położeniu. Działa do momentu aż zamkniesz okno.
C++// Punkty 1 //--------- #include <SDL.h> #include <iostream> #include <cstdlib> #include <ctime> using namespace std; //Rozmiar okienka const int W_W = 640; const int W_H = 480; int main(int argc, char* args[]) { // Inicjujemy SDL if(SDL_Init(SDL_INIT_VIDEO)) { cout << "SDL_Init Error: " << SDL_GetError() << endl; return 1; } // Tworzymy okno SDL_Window * w = SDL_CreateWindow("Grafika", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, W_W, W_H, 0); if(!w) { cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl; SDL_Quit(); return 2; } // Tworzymy kontekst graficzny SDL_Renderer * r = SDL_CreateRenderer(w,0,0); if(!r) { cout << "SDL_CreateRenderer Error: " << SDL_GetError() << endl; SDL_DestroyWindow(w); SDL_Quit(); return 3; } // Inicjujemy generator pseudolosowy srand(time(NULL)); // W pętli rysujemy przypadkowe punkty i czekamy na zdarzenie SDL_QUIT SDL_Event e; int x,y,cr,cg,cb,c = 0;; while(1) { cr = rand(); cg = rand(); cb = rand(); x = rand() % W_W; y = rand() % W_H; SDL_SetRenderDrawColor(r,cr,cg,cb,255); SDL_RenderDrawPoint(r,x,y); c++; if(c == 1000) { c = 0; SDL_RenderPresent(r); } if(SDL_PollEvent(&e) && e.type == SDL_QUIT) break; } // Usuwamy kontekst graficzny SDL_DestroyRenderer(r); // Usuwamy okno SDL_DestroyWindow(w); // Kończymy pracę z SDL2 SDL_Quit(); return 0; } |
Wyjaśnijmy nowe elementy:
#include <cstdlib> #include <ctime> |
W programie wykorzystujemy liczby pseudolosowe, których generacja wymaga dołączenia tych dwóch plików nagłówkowych. Plik nagłówkowy cstdlib definiuje standardowe funkcje biblioteki C. Plik nagłówkowy ctime definiuje funkcje biblioteki C odnoszące się do czasu. |
srand(time(NULL)); | Liczby pseudolosowe wyglądają jak
losowe (przypadkowe) lecz są
tworzone programowo przez funkcję, którą nazywamy
generatorem liczb pseudolosowych. Metod tworzenia liczb
pseudolosowych wymyślono dużo. Powinieneś o tych
liczbach dowiedzieć się na kursie programowania. Ich
własnością jest to, że każda kolejna liczba pseudolosowa
powstaje z poprzedniej wg określonego wzoru. Wynika z
tego, że tworzą one zawsze ten sam ciąg kolejnych
wartości, który po pewnym czasie zaczyna się powtarzać.
Pierwsza liczba pseudolosowa, z której powstają
wszystkie kolejne, nosi nazwę ziarna pseudolosowego
(ang. pseudorandom seed). Gdy
program jest uruchamiany ziarno otrzymuje zawsze tę samą
wartość początkową. Funkcja srand() ustawia
ziarno pseudolosowe na inną wartość, dzięki temu możemy
otrzymać ciąg pseudolosowy rozpoczynający się w innym
miejscu. Jeśli jako parametru tej funkcji użyjemy
funkcji czasu Tutaj nie ma to wielkiego znaczenia, ale wyobraź sobie, że tworzysz grę karcianą i chciałbyś, aby przy każdym uruchomieniu programu gracz otrzymywał inne karty, inaczej przestałoby to być po chwili zabawne, nieprawdaż? Dlatego w programach wykorzystujących generator pseudolosowy wywołuje się funkcję srand() z parametrem time(NULL) (funkcja time() wypełnia informacją o czasie strukturę, której adres otrzyma jako parametr. Jeśli otrzyma adres zerowy NULL, to nic nie będzie wypełniać, zwróci jedynie wartość czasu). Należy to zrobić tylko jeden raz gdzieś na początku programu. |
while(1) ... | To jest tzw. pętla nieskończona (ang. infinitive loop). Pętla typu while wykonuje się do momentu aż jej argument przyjmie wartość 0. 1 nie zmieni się w 0. Zatem pętla będzie się ciągle wykonywać aż ją przerwiemy z jej wnętrza. Pętle nieskończone często stosuje się w programowaniu wtedy, gdy nie wiemy (lub nie chcemy określać), ile razy pętla ma się wykonać. Tutaj pętla będzie wykonywana aż użytkownik zamknie okno. |
rand() | Wywołanie tej funkcji zwraca liczbę
pseudolosową w zakresie od 0 do RAND_MAX. Dla CodeBlocks
jest to zakres 0...32767. Nie oznacza to, że nie można
wygenerować większych liczb pseudolosowych. Nam tutaj
jednak taki zakres zupełnie wystarczy. Jeśli chcesz otrzymać liczbę pseudolosową w mniejszym zakresie od A do B, to stosujesz wzór:
liczba pseudolosowa od A do B = A + rand() % (B - A
+ 1)
W szczególności, jeśli A = 0, to:
liczba pseudolosowa od 0 do B = rand() % (B + 1)
|
cr = rand(); cg = rand(); cb = rand(); |
Losujemy składowe kolorów, Później zostaną one obcięte do rozmiaru 8-bitów. |
x = rand() % W_W; y = rand() % W_H; |
Losujemy współrzędne x i y punktów, tak aby znajdowały się w obszarze okna. |
SDL_SetRenderDrawColor(r,cr,cg,cb,255); SDL_RenderDrawPoint(r,x,y); |
Ustawiamy kolor piksela i rysujemy go w oknie na współrzędnych x,y. |
c++; if(c == 1000) { c = 0; SDL_RenderPresent(r); } |
Co 1000 narysowanych punktów przesyłamy teksturę okna do bufora ekranu. W ten sposób narysowane punkty stają się widoczne. Do liczenia punktów używamy licznika w zmiennej c. Chodzi tutaj o to, iż uaktualnienie treści okna zajmuje pewien czas i robienie tego przy każdym narysowanym pikselu byłoby dosyć wolne (możesz to sobie sprawdzić). Dzięki temu rozwiązaniu okno uaktualniamy hurtem po narysowaniu 1000 pikseli. |
if(SDL_PollEvent(&e) && e.type == SDL_QUIT) break; | Sprawdzamy, czy w kolejce znajduje się jakieś zdarzenie. Jeśli tak, to pobieramy jego strukturę do unii e i sprawdzamy, czy mamy do czynienia ze zdarzeniem SDL_QUIT. Jeśli tak, to przerywamy pętle, co spowoduje przejście wykonania do końcowej części programu. |
Następny program rysuje w oknie ruchome punkty, które odbijają się od jego krawędzi. Ruch uzyskamy przez cykliczne rysowanie obrazu punktów i uaktualnianie tego obrazu w buforze ekranu.
Jak rysować odbijający się punkt? Musimy operować na współrzędnych tego punktu. W tym celu zapamiętujemy cztery informacje:
Zasada jest następująca:
Aby było ciekawiej w programie będziemy animować 1000 takich punktów.
W projekcie sdl2 przekopiuj do edytora poniższy program, skompiluj go i uruchom:
C++// Punkty 2 //--------- #include <SDL.h> #include <iostream> #include <cstdlib> #include <ctime> using namespace std; // Rozmiar okienka const int W_W = 640; const int W_H = 480; // Liczba punktów const int N = 1000; int main(int argc, char* args[]) { // Tablice współrzędnych int x[N],y[N]; // Tablice przyrostów int dx[N],dy[N]; // Tablice składowych koloru Uint8 cr[N],cg[N],cb[N]; // Inicjujemy SDL if(SDL_Init(SDL_INIT_VIDEO)) { cout << "SDL_Init Error: " << SDL_GetError() << endl; return 1; } // Tworzymy okno SDL_Window * w = SDL_CreateWindow("Punkty", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, W_W, W_H, 0); if(!w) { cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl; SDL_Quit(); return 2; } // Tworzymy kontekst graficzny SDL_Renderer * r = SDL_CreateRenderer(w,0,0); if(!r) { cout << "SDL_CreateRenderer Error: " << SDL_GetError() << endl; SDL_DestroyWindow(w); SDL_Quit(); return 3; } // Inicjujemy generator pseudolosowy srand(time(NULL)); // Ustalamy wartości początkowe for(int i = 0; i < N; i++) { x[i] = rand() % W_W; y[i] = rand() % W_H; do dx[i] = -3 + (rand() % 7); while(!dx[i]); do dy[i] = -3 + (rand() % 7); while(!dy[i]); do cr[i] = rand(); while(!cr[i]); do cg[i] = rand(); while(!cg[i]); do cb[i] = rand(); while(!cb[i]); } SDL_Event e; while(1) { // Kasujemy poprzednio narysowane punkty SDL_SetRenderDrawColor(r,0,0,0,255); SDL_RenderClear(r); for(int i = 0; i < N; i++) { // Rysujemy punkt SDL_SetRenderDrawColor(r,cr[i],cg[i],cb[i],255); SDL_RenderDrawPoint(r,x[i],y[i]); // Wyliczamy nową pozycję if((x[i] + dx[i] >= W_W) || (x[i] + dx[i] < 0)) dx[i] = -dx[i]; x[i] += dx[i]; if((y[i] + dy[i] >= W_H) || (y[i] + dy[i] < 0)) dy[i] = -dy[i]; y[i] += dy[i]; } // Drobne opóźnienie SDL_Delay(30); // Uaktualniamy zawartość okna na ekranie SDL_RenderPresent(r); // Sprawdzamy, czy użytkownik nie zamyka programu if(SDL_PollEvent(&e) && e.type == SDL_QUIT) break; } // Usuwamy kontekst graficzny SDL_DestroyRenderer(r); // Usuwamy okno SDL_DestroyWindow(w); // Kończymy pracę z SDL2 SDL_Quit(); return 0; } |
Wyjaśnijmy nowe elementy w programie:
const int N = 1000; | Ta stała określa liczbę animowanych punktów. Możesz ją sobie zwiększyć lub zmniejszyć. Jednak przy przekroczeniu pewnej liczby punktów treść okna zaczyna mrugać, jeśli masz monitor LCD. Nie jest to wada programu, tylko właśnie monitora LCD. Jeśli obraz składa się z dużej liczby punktów różniących się znacznie jasnością, to niektóre monitory LCD powodują mruganie treści okna. Monitory kineskopowe nie wykazują tego efektu. Musisz sam poeksperymentować. |
int x[N],y[N]; int dx[N],dy[N]; Uint8 cr[N],cg[N],cb[N]; |
Dane dla punktów przechowuje kilka
tablic: x,y – współrzędne punktów dx,dy – przyrosty współrzędnych cr,cg,cb – składowe kolorów punktów |
x[i] = rand() % W_W; y[i] = rand() % W_H; |
W pierwszej pętli for inicjujemy komórki tablic odpowiednimi wartościami. Tutaj dla każdego punktu losowane jest położenie w oknie. |
do dx[i] = -3 + (rand() % 7);
while(!dx[i]); do dy[i] = -3 + (rand() % 7); while(!dy[i]); |
Te dwie pętle losują przyrosty: -3,-2,-1,1,2,3. Wartość 0 jest pomijana, ponieważ nie zmieniałaby położenia punktu. Dlatego losowanie odbywa się w pętli warunkowej, z której wyjście następuje, gdy wylosowany przyrost jest różny od 0. |
do cr[i] = rand();
while(!cr[i]); do cg[i] = rand(); while(!cg[i]); do cb[i] = rand(); while(!cb[i]); |
Na podobnej zasadzie losujemy składowe koloru, eliminując wartości 0. |
while(1)... | Animacja wykonywana jest w pętli nieskończonej, z której wyjście następuje po wykryciu zdarzenia SDL_QUIT. |
SDL_SetRenderDrawColor(r,0,0,0,255); SDL_RenderClear(r); |
Ustawiamy kolor czarny i wywołujemy funkcję SDL_RenderClear(), która wypełni tym kolorem całe okno. Sprawdź, co się stanie, gdy usuniesz z programu wywołanie tej funkcji (umieść je w komentarzu). |
for(int i = 0; i < N; i++)... | Pętla służy do przeglądania komórek tablic przechowujących informacje o punktach. |
SDL_SetRenderDrawColor(r,cr[i],cg[i],cb[i],255); SDL_RenderDrawPoint(r,x[i],y[i]); |
Ustawiamy kolor punktu z tablic składowych koloru i rysujemy w tym kolorze punkt na współrzędnych odczytanych z tablic x i y. |
if((x[i] + dx[i] >= W_W) || (x[i] + dx[i] < 0)) dx[i] = -dx[i]; | Tutaj sprawdzamy, czy po dodaniu do bieżącej współrzędnej przyrostu współrzędna ta nie wyjdzie poza granice okna. Jeśli tak, to zmieniamy znak przyrostu na przeciwny. |
x[i] += dx[i]; | Do współrzędnej dodajemy jej przyrost. W efekcie w następnym obiegu pętli while punkt pojawi się w innym miejscu obszaru graficznego okna. |
if((y[i] + dy[i] >= W_H) ||
(y[i] + dy[i] < 0)) dy[i] = -dy[i]; y[i] += dy[i]; |
To samo robimy z drugą współrzędną. Ponieważ zmianie ulegają obie współrzędne punktu, będzie się on poruszał po linii ukośnej. |
SDL_Delay(30); | Rysowanie punktów przebiega bardzo szybko, więc wprowadzamy nieco opóźnienia. Tutaj też możesz poeksperymentować z różnymi czasami opóźnień. |
SDL_RenderPresent(r); | Operacje były wykonywane na teksturze w pamięci VRAM. Przesyłamy zawartość tekstury do bufora ekranowego, aby uaktualnić treść okna. |
if(SDL_PollEvent(&e) && e.type == SDL_QUIT) break; | Pobieramy z kolejki zdarzenie do unii SDL_Event i sprawdzamy, czy jest to zdarzenie SDL_QUIT. Jeśli tak, przerywamy pętlę nieskończoną while, a to powoduje zakończenie programu. |
Trzeci program tworzy tzw. gradient, czyli płynne przejścia kolorów. Rysowane punkty mają kolor zależny od położenia na ekranie oraz od kolorów punktów w dwóch narożnikach okna: lewym górnym i prawym dolnym. Kolory narożników są płynnie zmieniane wg tej samej zasady, na której oparto poprzedni program. Do wartości składowych kolorów dodawane są przyrosty. Jeśli po dodaniu kolor wykraczałby poza dozwolony zakres od 0 do 255, to przyrost przed dodaniem ma zmieniany znak. Dzięki temu kolory składowe oscylują w górę i w dół pomiędzy wartościami granicznymi 0 i 255 i otrzymujemy różne kolory pośrednie, które płynnie się zmieniają.
Pozostaje problem wyznaczenia koloru punktu wewnątrz okna. Zastosowałem tutaj bardzo proste rozwiązanie: składowe koloru punktu zależą od składowych kolorów narożników oraz pozycji punktu.
Rozważmy najpierw przypadek uproszczony:
Punkt P porusza się od punktu A do punktu B po linii prostej. Odległość od punktu A do punktu P oznaczyliśmy literką d. Odległość między punktami A i B oznaczyliśmy literkami dd. W punkcie A wartość w wynosi a. W punkcie B wartość w wynosi b Ile wyniesie ta wartość w punkcie P, jeśli zmienia się liniowo wzdłuż drogi od punktu A do punktu B?
To proste zadanie geometryczne:
W punkcie P mamy z proporcji:
W naszym programie jako odległość d weźmiemy sumę współrzędnych x i y punktu P. Jako dd weźmiemy sumę szerokości i wysokości okna pomniejszoną o 2 (dlaczego?). Na tej podstawie policzymy wartość składowej koloru w, jeśli a jest składową koloru punktu A, a b jest składową koloru punktu B:
Skopiuj do edytora poniższy program, skompiluj go i uruchom:
C++// Punkty 3 //--------- #include <SDL.h> #include <iostream> #include <cstdlib> #include <ctime> using namespace std; // Rozmiar okienka const int W_W = 640; const int W_H = 480; int main(int argc, char* args[]) { // Inicjujemy SDL if(SDL_Init(SDL_INIT_VIDEO)) { cout << "SDL_Init Error: " << SDL_GetError() << endl; return 1; } // Tworzymy okno SDL_Window * w = SDL_CreateWindow("Punkty", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, W_W, W_H, 0); if(!w) { cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl; SDL_Quit(); return 2; } // Tworzymy kontekst graficzny SDL_Renderer * r = SDL_CreateRenderer(w,0,0); if(!r) { cout << "SDL_CreateRenderer Error: " << SDL_GetError() << endl; SDL_DestroyWindow(w); SDL_Quit(); return 3; } // Inicjujemy generator pseudolosowy srand(time(NULL)); // Ustalamy wartości początkowe int ce[2][3],cd[2][3],c[3],dc[3],i,j,x,y,d,dd,nc; for(i = 0; i < 2; i++) for(j = 0; j < 3; j++) { ce[i][j] = rand() % 256; do cd[i][j] = -2 + rand() % 5; while(!cd[i][j]); } dd = W_W + W_H - 2; SDL_Event e; while(1) { // Wyliczamy różnice kolorów for(i = 0; i < 3; i++) dc[i] = ce[1][i] - ce[0][i]; // Rysujemy punkty for(x = 0; x < W_W; x++) for(y = 0; y < W_H; y++) { d = x + y; for(i = 0; i < 3; i++) c[i] = ce[0][i] + (dc[i] * d) / dd; SDL_SetRenderDrawColor(r,c[0],c[1],c[2],255); SDL_RenderDrawPoint(r,x,y); } // Modyfikujemy kolory for(i = 0; i < 2; i++) for(j = 0; j < 3; j++) { nc = ce[i][j] + cd[i][j]; if((nc < 0) || (nc > 255)) cd[i][j] = -cd[i][j]; ce[i][j] += cd[i][j]; } // Uaktualniamy zawartość okna na ekranie SDL_RenderPresent(r); // Sprawdzamy, czy użytkownik nie zamyka programu if(SDL_PollEvent(&e) && e.type == SDL_QUIT) break; } // Usuwamy kontekst graficzny SDL_DestroyRenderer(r); // Usuwamy okno SDL_DestroyWindow(w); // Kończymy pracę z SDL2 SDL_Quit(); return 0; } |
Wyjaśnijmy elementy programu:
ce[2][3] | Składowe kolorów punktów narożnikowych
przechowywane są w tablicy ce. Rozwiązanie takie
przyjąłem dlatego, iż obliczenia dla składowych
czerwonej, zielonej i niebieskiej są dokładnie takie
same. Jednym z powodów stosowania tablic jest
uproszczenie obliczeń. Zamiast wykonywać 3 razy to samo,
wykorzystamy pętlę. ce[0] określa składowe koloru pierwszego narożnika okna (lewy górny):
Podobnie ce[1] określa składowe koloru drugiego narożnika (prawy dolny):
|
||
cd[2][3] | Ta tablica przechowuje przyrosty dla
składowych koloru narożników:
|
||
c[3] | Przechowuje składowe kolorów rysowanych
punktów:
|
||
dc[3] | Przechowuje różnicę składowych koloru
narożników. Wartość ta występuje w podanym wyżej wzorze
jako b - a:
|
||
for(i = 0; i < 2; i++) for(j = 0; j < 3; j++) { ce[i][j] = rand() % 256; do cd[i][j] = -2 + rand() % 5; while(!cd[i][j]); } |
Te dwie pętle losują wartości początkowe składowych kolorów narożników okna oraz ich przyrosty od -2 do 2. Przyrost 0 jest pomijany. | ||
dd = W_W + W_H - 2; | Odległość między narożnikami. | ||
for(i = 0; i < 3; i++) dc[i] = ce[1][i] - ce[0][i]; |
Pętla oblicza różnice składowych kolorów narożników i umieszcza je w tablicy dc. | ||
for(x = 0; x < W_W; x++) for(y = 0; y < W_H; y++) ... |
Te dwie pętle przebiegają po wszystkich współrzędnych punktów w obszarze okna. | ||
d = x + y; | Odległość od pierwszego narożnika (lewy górny). | ||
for(i = 0; i < 3; i++) c[i] = ce[0][i] + (dc[i] * d) / dd; |
W tej pętli zostają wyliczone składowe
koloru punktu na podstawie kolorów narożników oraz
położenia punktu. Jest to dokładnie podany przez nas
wzór dla poszczególnych składowych koloru:
Zwróć uwagę na kolejność mnożenia i dzielenia. Jest ważna! (dlaczego?) |
||
SDL_SetRenderDrawColor(r,c[0],c[1],c[2],255); SDL_RenderDrawPoint(r,x,y); |
Gdy składowe koloru zostaną policzone, ustawiamy ten kolor i rysujemy punkt. |
A teraz kilka zadań dla ciebie:
Dopisz do programu fragment, który narysuje na każdym kadrze ładną siatkę kropek w kolorze białym o oczku 8 x 8 pikseli:
Zmień kropki na kratkę o oczku 16 x 16 pikseli:
Zmień rysowanie punktów, tak aby powstawały w siatce 2 x 2 piksele:
Oprócz rysowania pojedynczych punktów SDL2 oferuje funkcję rysującą ciąg punktów, których współrzędne znajdują się w tablicy o elementach typu SDL_Pont:
struct SDL_Point { int x; int y; };
Funkcja posiada składnię:
SDL_RenderDrawPoints(r,p,n)
r – wskaźnik
struktury SDL_Renderer z kontekstem graficznym,
p – tablica
elementów typu SDL_Point, które przechowują współrzędne punktów
do wyświetlenia.
n – liczba punktów w tablicy.
Punkty rysowane są w kolorze, który jest ustawiany przez funkcję SDL_SetRenderDrawColor().
Program kolejny będzie rysował przesuwające się tło, np. gwiazd. Punkty będą animowane w ten sposób, iż ich współrzędne są zwiększane o ten sam przyrost dla wszystkich punktów. Jednakże po zmianie sprawdzone zostanie, czy współrzędna nie wyszła poza krawędź okna. Jeśli tak, to będzie zmniejszana odpowiednio o szerokość okna dla współrzędnych poziomych lub o wysokość okna dla współrzędnych pionowych. W efekcie punkt pojawi się po drugiej stronie okienka i znów rozpocznie swój ruch.
Skopiuj do edytora poniższy program, skompiluj go i uruchom:
C++// Punkty 4 //--------- #include <SDL.h> #include <iostream> #include <cstdlib> #include <ctime> using namespace std; // Rozmiar okienka const int W_W = 640; const int W_H = 480; // Liczba punktów const int N = 1000; int main(int argc, char* args[]) { // Inicjujemy SDL if(SDL_Init(SDL_INIT_VIDEO)) { cout << "SDL_Init Error: " << SDL_GetError() << endl; return 1; } // Tworzymy okno SDL_Window * w = SDL_CreateWindow("Punkty", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, W_W, W_H, 0); if(!w) { cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl; SDL_Quit(); return 2; } // Tworzymy kontekst graficzny SDL_Renderer * r = SDL_CreateRenderer(w,0,SDL_RENDERER_PRESENTVSYNC); if(!r) { cout << "SDL_CreateRenderer Error: " << SDL_GetError() << endl; SDL_DestroyWindow(w); SDL_Quit(); return 3; } // Inicjujemy generator pseudolosowy srand(time(NULL)); // Ustalamy wartości początkowe // Tablica punktów SDL_Point P[N]; for(int i = 0; i < N; i++) { P[i].x = rand() % W_W; P[i].y = rand() % W_H; } // Przyrosty int dx = 1 + rand() % 2; int dy = 1 + rand() % 2; // Animacja SDL_Event e; while(1) { // Kasujemy poprzednio narysowane punkty SDL_SetRenderDrawColor(r,0,0,0,255); SDL_RenderClear(r); // Rysujemy punkty SDL_SetRenderDrawColor(r,255,255,255,255); SDL_RenderDrawPoints(r,P,N); // Przesuwamy punkty for(int i = 0; i < N; i++) { P[i].x += dx; if(P[i].x >= W_W) P[i].x -= W_W; P[i].y += dy; if(P[i].y >= W_H) P[i].y -= W_H; } // Drobne opóźnienie SDL_Delay(30); // Uaktualniamy zawartość okna na ekranie SDL_RenderPresent(r); // Sprawdzamy, czy użytkownik nie zamyka programu if(SDL_PollEvent(&e) && e.type == SDL_QUIT) break; } // Usuwamy kontekst graficzny SDL_DestroyRenderer(r); // Usuwamy okno SDL_DestroyWindow(w); // Kończymy pracę z SDL2 SDL_Quit(); return 0; } |
Elementy programu:
const int N = 1000; | Liczba punktów, które będą przesuwane w oknie. |
SDL_Point P[N]; | Współrzędne punktów przechowuje tablica P. Każdy element tej tablicy jest strukturą SDL_Point, która zawiera dwa pola typu int: x i y. |
for(int i = 0; i < N; i++) { P[i].x = rand() % W_W; P[i].y = rand() % W_H; } |
losujemy współrzędne punktów w obszarze okna i zapamiętujemy je w tablicy P. |
SDL_SetRenderDrawColor(r,0,0,0,255); SDL_RenderClear(r); |
Ustawiamy kolor czarny i wypełniamy nim cały obszar okna. W ten sposób pozbędziemy się z tekstury poprzednio narysowanych na niej punktów. |
SDL_SetRenderDrawColor(r,255,255,255,255); SDL_RenderDrawPoints(r,P,N); |
Ustawiamy kolor biały i rysujemy hurtem wszystkie punkty w tablicy P. |
for(int i = 0; i < N; i++) { P[i].x += dx; if(P[i].x >= W_W) P[i].x -= W_W; P[i].y += dy; if(P[i].y >= W_H) P[i].y -= W_H; } |
Modyfikujemy współrzędne wszystkich punktów. Dodajemy do nich przesunięcia: dx do współrzędnych poziomych x i dy do współrzędnych pionowych y. Następnie sprawdzamy, czy dana współrzędna nie wyszła poza krawędź okna. Jeśli tak, to ją odpowiednio zmniejszamy: współrzędne x zmniejszamy o szerokość okna, a współrzędne y zmniejszamy o wysokość okna. W ten sposób punkt pojawi się z drugiej strony okna i nigdy nie wyjdzie poza jego obszar. |
Kolejny program będzie obracał punkty wokół środka ekranu. Tego typu operację wykonuje się zwykle za pomocą rachunku macierzowego przez złożenie przekształceń. Jednak w tym miejscu kursu nie będę jeszcze wprowadzał macierzy przekształceń, ponieważ na to zagadnienie przeznaczę jeden cały rozdział. Obrót wykonamy przez wyprowadzenie wzoru na współrzędne punktu. Najpierw rozważmy prostszy przypadek: obrót punktu wokół osi układu współrzędnych o zadany kąt φ:
Jeśli znamy kat φ i współrzędne x,y punktu P, to naszym zadaniem będzie obliczenie współrzędnych x' i y' punktu P', który powstaje przez obrót punktu P o kąt φ wokół środka układu współrzędnych. Zwróć uwagę, że oba punkty P i P' znajdują się na obwodzie tego samego koła o promieniu r. Dla okręgu o środku w punkcie (0,0) układu współrzędnych mamy:
Z tego wzoru wyliczamy promień okręgu:
Z definicji funkcji trygonometrycznych mamy:
Teraz sięgamy do wzorów funkcji trygonometrycznych dla sumy kątów:
Dokonujemy podstawień:
Pozbywamy się promienia i otrzymujemy wzór ostateczny:
Zadanie rozwiązane. Co jednak zrobić, jeśli środek obrotu nie leży w środku układu współrzędnych? Tutaj z pomocą przychodzi przekształcenie, zwane translacją, które polega na przesunięciu punktu, tak aby jego środek obrotu znalazł się w środku układu współrzędnych. Po przesunięciu obracamy punkt i dokonujemy translacji odwrotnej, czyli przesuwamy środek obrotu punktu na poprzednie miejsce. Wygląda to tak (sx i sy to współrzędne środka obrotu):
Przenosimy współrzędne punktu, tak aby środek obrotu trafił do środka układu współrzędnych:
Współrzędne wykorzystujemy do obrotu:
Przywracamy oryginalne położenie środka obrotu:
Po wykonaniu podstawień, otrzymujemy ostateczne wzory:
Skopiuj do edytora poniższy program, skompiluj go i uruchom:
C++// Punkty 5 //--------- #include <SDL.h> #include <iostream> #include <cstdlib> #include <ctime> #include <cmath> using namespace std; // Rozmiar okienka const int W_W = 640; const int W_H = 480; // Liczba punktów const int N = 10000; // Liczba obrotów cząstkowych na jeden pełny obrót const int RN = 3000; int main(int argc, char* args[]) { // Inicjujemy SDL if(SDL_Init(SDL_INIT_VIDEO)) { cout << "SDL_Init Error: " << SDL_GetError() << endl; return 1; } // Tworzymy okno SDL_Window * w = SDL_CreateWindow("Punkty", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, W_W, W_H, 0); if(!w) { cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl; SDL_Quit(); return 2; } // Tworzymy kontekst graficzny SDL_Renderer * r = SDL_CreateRenderer(w,0,SDL_RENDERER_PRESENTVSYNC); if(!r) { cout << "SDL_CreateRenderer Error: " << SDL_GetError() << endl; SDL_DestroyWindow(w); SDL_Quit(); return 3; } // Inicjujemy generator pseudolosowy srand(time(NULL)); // Ustalamy wartości początkowe // Tablice punktów SDL_Point O[N],P[N]; for(int i = 0; i < N; i++) { O[i].x = -W_W + rand() % (3 * W_W); O[i].y = -W_H + rand() % (3 * W_H); } // Środek obrotu const int sx = W_W / 2; const int sy = W_H / 2; // Kąt obrotu i jego przyrost double fi = 0; double df = 2 * M_PI / RN; // Animacja SDL_Event e; while(1) { // Kasujemy poprzednio narysowane punkty SDL_SetRenderDrawColor(r,0,0,0,255); SDL_RenderClear(r); // Obracamy punkty double cos_fi = cos(fi); double sin_fi = sin(fi); for(int i = 0; i < N; i++) { P[i].x = (O[i].x - sx) * cos_fi - (O[i].y - sy) * sin_fi + sx; P[i].y = (O[i].y - sy) * cos_fi + (O[i].x - sx) * sin_fi + sy; } // Rysujemy obrócone punkty SDL_SetRenderDrawColor(r,255,255,255,255); SDL_RenderDrawPoints(r,P,N); // Zwiększamy kąt obrotu fi += df; if(fi >= 2 * M_PI) fi = 0; // Drobne opóźnienie SDL_Delay(10); // Uaktualniamy zawartość okna na ekranie SDL_RenderPresent(r); // Sprawdzamy, czy użytkownik nie zamyka programu if(SDL_PollEvent(&e) && e.type == SDL_QUIT) break; } // Usuwamy kontekst graficzny SDL_DestroyRenderer(r); // Usuwamy okno SDL_DestroyWindow(w); // Kończymy pracę z SDL2 SDL_Quit(); return 0; } |
Wyjaśnijmy elementy programu:
#include <cmath> | Do programu musimy dołączyć plik z definicjami funkcji matematycznych sin() i cos(). |
const int RN = 3000; | Stała określa liczbę obrotów cząstkowych na jeden obrót o kąt pełny. Im większa wartość, tym płynniejszy jest obrót punktów. |
SDL_Point O[N],P[N]; | Dane są przechowywane w dwóch tablicach. Tablica O zawiera podstawowe pozycje punktów i służy jako baza do obliczeń. Tablica P zawiera pozycje punktów po wykonaniu obrotu. Służy do wyświetlenia punktów. |
for(int i = 0; i < N; i++) { O[i].x = -W_W + rand() % (3 * W_W); O[i].y = -W_H + rand() % (3 * W_H); } |
Ta pętla losuje położenia punktów w obszarze wykraczającym poza okno (aby przy obrotach nie było widać pustych obszarów) i umieszcza współrzędne w tablicy O. Jeśli punkt jest poza obszarem graficznym okna, to nie będzie rysowany. Nie musimy się tym przejmować. |
const int sx = W_W / 2; const int sy = W_H / 2; |
Definiujemy stałe określające położenie środka obrotu. Jest to oczywiście środek okna. |
double fi = 0; double df = 2 * M_PI / RN; |
Obroty będą wykonywane o kąt fi,
który będziemy stopniowo zwiększać, aż osiągnie kąt
pełny. Wtedy zostanie wykonany pełen obrót. Zmienna
df określa przyrost kąta przy każdym obrocie. Stała M_PI to wartość liczby π. Zdefiniowana jest w pliku nagłówkowym cmath. |
SDL_SetRenderDrawColor(r,0,0,0,255); SDL_RenderClear(r); |
Czyścimy okno z poprzednio narysowanych punktów. Ustawiamy kolor czarny i wypełniamy nim całą powierzchnię graficzną okna. |
double cos_fi = cos(fi); double sin_fi = sin(fi); |
We wzorze na współrzędne obróconego punktu występują funkcje sin() i cos() dla kąta fi. Kąt ten zmienia się dopiero po przeliczeniu współrzędnych wszystkich punktów. Aby nie tracić czasu na obliczanie funkcji trygonometrycznych dla każdego punktu, zapamiętujemy ich wartości w zmiennych sin_fi i cos_fi. |
for(int i = 0; i < N; i++) { P[i].x = (O[i].x - sx) * cos_fi - (O[i].y - sy) * sin_fi + sx; P[i].y = (O[i].y - sy) * cos_fi + (O[i].x - sx) * sin_fi + sy; } |
Wyliczamy współrzędne punktów po obrocie i zapisujemy je do tablicy P. Dane pobieramy we wzorze z tablicy O, która przechowuje położenia oryginalne punktów. Takie rozwiązanie eliminuje błędy obliczeniowe. |
SDL_SetRenderDrawColor(r,255,255,255,255); SDL_RenderDrawPoints(r,P,N); |
Gdy wszystkie punkty zostały obliczone, ustawiamy kolor biały i hurtem rysujemy zawartość całej tablicy P. |
fi += df; if(fi >= 2 * M_PI) fi = 0; |
Zwiększamy kąt obrotu o wyliczony przyrost. W ten sposób następny obrót będzie o większy kąt. Jednak, gdy kat osiągnie wartość kata pełnego, zerujemy go. W sumie nie jest to konieczne, ale w ten sposób zmniejszamy ewentualne błędy zaokrągleń kąta. |
Reszta programu jest już standardowa.
Jako ćwiczenie spróbuj zmodyfikować ten program, tak aby środek obrotu nie był stały, lecz poruszał się w oknie. Wykorzystaj poprzednie programy, gdzie odbijane były punkty od krawędzi okna.
Poniższy program wypełnia okno pikselami o różnych kolorach, a następnie dokonuje cyklicznej rotacji ich składowych kolorów: r → g; g → b; b → r. Wymaga to odczytania składowych koloru poszczególnych pikseli okna.
W CodeBlocks utwórz nowy projekt sdl2, skopiuj do edytora poniższy program, skompiluj i uruchom go:
C++// Odczyt punktów 1 //----------------- #include <SDL.h> #include <iostream> #include <cstdlib> #include <ctime> using namespace std; // Rozmiar okienka const int W_W = 640; const int W_H = 480; // Liczba punktów const int N = 5000; // Funkcja stawia piksel o zadanym kolorze na pozycji x,y obrazu //-------------------------------------------------------------- void Plotxyc(SDL_Surface * s, int x, int y, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { Uint32 color, * addr; // Obliczamy adres piksela addr = (Uint32 *)s->pixels + x + y * s->w; // Obliczamy kod piksela color = (r << s->format->Rshift) & (s->format->Rmask); color |= (g << s->format->Gshift) & (s->format->Gmask); color |= (b << s->format->Bshift) & (s->format->Bmask); color |= (a << s->format->Ashift) & (s->format->Amask); // Umieszczamy piksel na obrazie * addr = color; } // Funkcja odczytuje kolory piksela na pozycji x,y //------------------------------------------------ void ReadPixelColor(SDL_Surface * s, int x, int y, Uint8 & r, Uint8 & g, Uint8 & b, Uint8 & a) { Uint32 color, * addr; // Obliczamy adres piksela addr = (Uint32 *)s->pixels + x + y * s->w; // Odczytujemy kolor piksela color = * addr; // Wydobywamy składowe z kodu piksela r = (color & s->format->Rmask) >> s->format->Rshift; g = (color & s->format->Gmask) >> s->format->Gshift; b = (color & s->format->Bmask) >> s->format->Bshift; a = (color & s->format->Amask) >> s->format->Ashift; } int main(int argc, char* args[]) { // Inicjujemy SDL if(SDL_Init(SDL_INIT_VIDEO)) { cout << "SDL_Init Error: " << SDL_GetError() << endl; return 1; } // Tworzymy okno SDL_Window * w = SDL_CreateWindow("Punkty", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, W_W, W_H, 0); if(!w) { cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl; SDL_Quit(); return 2; } // Powierzchnia graficzna okna SDL_Surface * s = SDL_GetWindowSurface(w); // Sprawdzamy, czy piksele są 32-bitowe. Jeśli nie, kończymy if(s->format->BitsPerPixel != 32) { cout << "Pixels not 32 bits in size" << endl; SDL_DestroyWindow(w); SDL_Quit(); return 2; } // Inicjujemy generator pseudolosowy srand(time(NULL)); // Wypełniamy okno pikselami o róźnych kolorach SDL_LockSurface(s); for(int i = 0; i < N; i++) Plotxyc(s,rand()%W_W,rand()%W_H,rand(),rand(),rand(),255); SDL_UnlockSurface(s); // Animacja SDL_Event e; while(1) { // Wykonujemy rotację składowych kolorów wszystkich pikseli na powierzchni graficznej Uint8 r,g,b,a; SDL_LockSurface(s); for(int x = 0; x < W_W; x++) for(int y = 0; y < W_H; y++) { ReadPixelColor(s,x,y,r,g,b,a); Plotxyc(s,x,y,b,r,g,a); } SDL_UnlockSurface(s); SDL_UpdateWindowSurface(w); SDL_Delay(250); // Sprawdzamy, czy użytkownik nie zamyka programu if(SDL_PollEvent(&e) && e.type == SDL_QUIT) break; } // Usuwamy okno SDL_DestroyWindow(w); // Kończymy pracę z SDL2 SDL_Quit(); return 0; } |
Opiszmy nowe elementy programu:
void Plotxyc(...) | Funkcja rysuje piksel na powierzchni SDL_Surface. Opisaliśmy ją w pierwszym programie tego rozdziału. |
void ReadPixelColor(...) | Funkcja odczytuje składowe koloru piksela na zadanej pozycji powierzchni SDL_Surface okna. Poniżej jej opis: |
Uint32 color, * addr; | Zmienna color będzie zawierała kod koloru piksela, zmienna addr będzie zawierała jego adres w pamięci RAM. |
addr = (Uint32 *)s->pixels + x + y * s->w; | Obliczamy adres piksela w RAM. |
color = * addr; | Pobieramy kod koloru piksela |
r = (color & s->format->Rmask)
>> s->format->Rshift; g = (color & s->format->Gmask) >> s->format->Gshift; b = (color & s->format->Bmask) >> s->format->Bshift; a = (color & s->format->Amask) >> s->format->Ashift; |
Na podstawie informacji o formacie piksela w strukturze SDL_Surface z kodu koloru wydobywamy kolejne składowe. |
SDL_LockSurface(s); for(int i = 0; i < N; i++) Plotxyc(s,rand()%W_W,rand()%W_H,rand(),rand(),rand(),255); SDL_UnlockSurface(s); |
Rysujemy na powierzchni piksele w różnych kolorach i na różnych pozycjach. |
Uint8 r,g,b,a; | Te zmienne przechowują składowe koloru piksela. |
SDL_LockSurface(s); for(int x = 0; x < W_W; x++) for(int y = 0; y < W_H; y++) { ReadPixelColor(s,x,y,r,g,b,a); Plotxyc(s,x,y,b,r,g,a); } |
Przeglądamy całą powierzchnię okna punkt po punkcie i wykonujemy rotację składowych koloru |
SDL_UnlockSurface(s); SDL_UpdateWindowSurface(w); SDL_Delay(250); |
Uaktualniamy obraz okna z powierzchni graficznej i czekamy 1/4 sekundy. |
Reszta programu jest standardowa
Powierzchnia SDL_Surface znajduje się w pamięci RAM i akcelerator grafiki nie ma do niej bezpośredniego dostępu. W bibliotece SDL2 brak funkcji odczytującej z tekstury w pamięci VRAM kolor pojedynczego piksela, co nie oznacza, iż operacji tej nie da się wykonać. Można to zrobić na kilka sposobów, jednak przesyłanie danych pomiędzy pamięciami RAM i VRAM nie jest tak efektywne, jak działanie tylko na pamięci VRAM przez układy logiczne akceleratora. Dlatego, jeśli program chce przetworzyć dużą liczbę pikseli, dobrym rozwiązaniem jest tymczasowe skopiowanie pikseli całej tekstury okna do pamięci RAM, wykonanie modyfikacji, a następnie uaktualnienie hurtem pikseli tekstury w pamięci VRAM karty graficznej. Takimi operacjami zajmiemy się w dalszej części kursu.
Program poniżej jest wersją poprzedniego programu, jednak tutaj nie tworzymy powierzchni SDL_Surface w pamięci RAM, lecz wykorzystujemy kontekst graficzny i operacje przesłania z pamięci VRAM do RAM. Skopiuj program do edytora, skompiluj i uruchom go:
C++// Odczyt punktów 2 //----------------- #include <SDL.h> #include <iostream> #include <cstdlib> #include <ctime> using namespace std; // Rozmiar okienka const int W_W = 640; const int W_H = 480; // Liczba punktów const int N = 5000; int main(int argc, char* args[]) { // Inicjujemy SDL if(SDL_Init(SDL_INIT_VIDEO)) { cout << "SDL_Init Error: " << SDL_GetError() << endl; return 1; } // Tworzymy okno SDL_Window * w = SDL_CreateWindow("Punkty", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, W_W, W_H, 0); if(!w) { cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl; SDL_Quit(); return 2; } // Tworzymy kontekst graficzny SDL_Renderer * r = SDL_CreateRenderer(w,0,SDL_RENDERER_PRESENTVSYNC); if(!r) { cout << "SDL_CreateRenderer Error: " << SDL_GetError() << endl; SDL_DestroyWindow(w); SDL_Quit(); return 3; } // Inicjujemy generator pseudolosowy srand(time(NULL)); // Wypełniamy okno pikselami o róźnych kolorach for(int i = 0; i < N; i++) { SDL_SetRenderDrawColor(r,rand(),rand(),rand(),255); SDL_RenderDrawPoint(r,rand() % W_W,rand() % W_H); } // Tablica kolorów punktów SDL_Color pixels[W_H][W_W]; SDL_Rect rect; rect.x = rect.y = 0; rect.w = W_W; rect.h = W_H; // Animacja SDL_Event e; while(1) { // Czytamy piksele z tekstury do tablicy SDL_RenderReadPixels(r,&rect,SDL_PIXELFORMAT_ABGR8888,pixels,W_W * 4); for(int x = 0; x < W_W; x++) for(int y = 0; y < W_H; y++) { SDL_SetRenderDrawColor(r,pixels[y][x].b,pixels[y][x].r,pixels[y][x].g,255); SDL_RenderDrawPoint(r,x,y); } SDL_RenderPresent(r); SDL_Delay(250); // Sprawdzamy, czy użytkownik nie zamyka programu if(SDL_PollEvent(&e) && e.type == SDL_QUIT) break; } // Usuwamy kontekst graficzny SDL_DestroyRenderer(r); // Usuwamy okno SDL_DestroyWindow(w); // Kończymy pracę z SDL2 SDL_Quit(); return 0; } |
Elementy programu:
for(int i = 0; i < N; i++) { SDL_SetRenderDrawColor(r,rand(),rand(),rand(),255); SDL_RenderDrawPoint(r,rand() % W_W,rand() % W_H); } |
Rysujemy na powierzchni okna N punktów o różnych kolorach i na różnych pozycjach | |
SDL_Color pixels[W_H][W_W]; | Tablica pixels jest dwuwymiarową
tablicą struktur SDL_Color, tzn. element
pixels[y][x] będzie strukturą SDL_Color dla piksela
na pozycji x,y w oknie. Dlaczego
współrzędne są odwrócone? Jest to konsekwencja zapisu
obrazu w pamięci. Obraz zapisywany jest kolejnymi
liniami poziomymi. Linie odpowiadają współrzędnym y.
Zawartość linii to piksele, które odpowiadają
współrzędnym x. Stąd element pixels[y][x]
odnosi się do piksela leżącego na pozycji x w
linii o numerze y. Struktura SDL_Color ma cztery pola typu Uint8:
|
|
SDL_Rect rect; rect.x = rect.y = 0; rect.w = W_W; rect.h = W_H; |
Ta struktura zawiera definicję prostokąta obejmującego całą powierzchnię okna. Potrzebujemy jej do kopiowania pikseli z tekstury w VRAM do tablicy w RAM. | |
SDL_RenderReadPixels(r,&rect,SDL_PIXELFORMAT_ABGR8888,pixels,W_W * 4); | Funkcja SDL_RenderReadPixels()
odczytuje z tekstury okna prostokątny obszar pikseli i
umieszcza go we wskazanym miejscu w pamięci RAM.
Parametry są następujące:
Funkcja kopiuje piksele z tekstury do tablicy pixels, z której będziemy mogli wygodnie odczytywać ich kolory. |
|
for(int x = 0; x < W_W; x++) for(int y = 0; y < W_H; y++) { SDL_SetRenderDrawColor(r,pixels[y][x].b,pixels[y][x].r,pixels[y][x].g,255); SDL_RenderDrawPoint(r,x,y); } |
Przeglądamy tablicę pixels i wykonujemy na podstawie jej zawartości rotację składowych koloru poszczególnych pikseli na teksturze. |
Reszta programu jest standardowa.
SDL_RenderClear(r) – wypełnia całą powierzchnię graficzną okna bieżącym kolorem.
r – wskaźnik struktury SDL_Renderer
SDL_RenderDrawPoint(r,x,y) – rysuje punkt w bieżącym kolorze.
r – wskaźnik struktury SDL_Renderer
x,y – współrzędne punktu
SDL_RenderDrawPoints(r,p,n) – rysuje zadaną liczbę punktów z tablicy
r – wskaźnik struktury SDL_Renderer
p – tablica struktur typu SDL_Point, która definiuje
współrzędne punktów
n – liczba wierzchołków w tablicy p
SDL_RenderReadPixels(r,rt,f,px,p)
r – wskaźnik struktury SDL_Renderer
rt – wskaźnik struktury SDL_Rect, która definiuje
obszar do odczytania w oknie.
f – docelowy format pikseli.
px – wskaźnik obszaru, w którym zostaną umieszczone
kody kolorów pikseli.
p – liczba bajtów na jeden wiersz pikseli w obszarze
docelowym
![]() |
Zespół Przedmiotowy Chemii-Fizyki-Informatyki w I Liceum Ogólnokształcącym im. Kazimierza Brodzińskiego w Tarnowie ul. Piłsudskiego 4 ©2023 mgr Jerzy Wałaszek |
Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone
pod warunkiem podania źródła oraz niepobierania za to pieniędzy.
Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.
Informacje dodatkowe.