Skocz do zawartości

Adder

Użytkownicy
  • Zawartość

    134
  • Rejestracja

  • Ostatnio

Ostatnio na profilu byli

Blok z ostatnio odwiedzającymi jest wyłączony i nie jest wyświetlany innym użytkownikom.

Adder's Achievements

5/10

5/10 (5/10)

31

Reputacja

  1. Oczywiście, ale to już jest trochę bardziej zaawansowane niż średnio-. Trzeba wejść w klasy, kolizje i algorytm grawitacji obiektów 3D. Zrobiłem kiedyś taki model. To co się dzieje z obiektami jest nie do ogarnięcia. Zaczynają żyć własnym życiem. Uciekają poza scenę, zderzają się, poruszają po najdziwniejszych torach. Można byłoby się na tym doktoryzować.
  2. Część Czwarta W poprzedniej części omówione zostało wypełnianie obiektów trójwymiarowych teksturą oraz przedstawione wybrane funkcje światła. W ostatniej już, czwartej części, program uzupełnimy o funkcje umożliwiające obejrzenie całego modelu ze wszystkich stron przy wykorzystaniu komputerowej myszki. Obrotu widoku sceny można dokonać na 2 podstawowe sposoby: 1. bezpośrednie przekształcenia obiektów w przestrzeni trójwymiarowej; 2. zmianę położenia obserwatora (kamery) patrzącego na nieruchome obiekty. Do programu zaimplementowane zostały obydwa sposoby. Przy czym, transformacja obiektów została wprowadzona w wersji: (1) - „dla wymagających”. Natomiast, przesunięcia kamery w wersji (2) - „dla leniwych”, przy wykorzystaniu biblioteki zewnętrznej. Bez względu na wybraną metodę końcowy efekt będzie podobny, ale nie identyczny. PMatrix3D, PVector W pierwszej kolejności zajmiemy się „wersją dla wymagających”. Naszym celem jest wyposażenie programu w możliwość jednoczesnego obracania wszystkich elementów znajdujących się na scenie wokół dowolnej osi i o dowolny kąt za pośrednictwem myszki. Innymi słowy, chcemy dokonywać transformacji położenia wszystkich obiektów razem, względem nieruchomego obserwatora zależnie od położenia kursora myszki. Rozwiązania intuicyjne tego zadania nie zawsze prowadzą do oczekiwanych wyników. Najczęściej, o ile nie zostaną zastosowane restrykcyjne reguły transformacji, układ Ziemia-Słońce będzie zmieniał swoje położenie z trudnymi do okiełznania aberracjami… Na szczęście teorię przekształceń w przestrzeni 3D można znaleźć w każdym akademickim podręczniku geometrii lub ją sobie wymyślić i tym samym nie ma potrzeby jej tutaj przytaczać. Bardziej zainteresowanych tematem z przyjemnością odsyłam do artykułu: 3D Rotations in Processing (Vectors, Matrices, Quaternions) , można w nim znaleźć dość przejrzyście wyjaśnioną teorię stosowaną do przesunięć w przestrzeni 3D, wraz z propozycjami jej implementacji. Tutaj zajmiemy się wyłącznie zastosowaniami praktycznymi. Krótki, prosty i co najważniejsze efektywny algorytm obrotu przestrzeni 3D o zadany wektor jest następujący: Krok 1 Zainicjowanie macierzy współrzędnych jednorodnych (MATRIX): Inicjalizacja macierzy będzie polegała na przypisaniu macierzy współrzędnej czterowymiarowej macierzy jednostkowej: PMatrix3D MATRIX=new PMatrix3D // Inicjalizacja obiektu MATRIX klasy PMatrix3D (macierz przestrzeni 3D w ujęciu jednorodnym) (1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1); Wykorzystana została do tego nieudokumentowana klasa PMatrix3D (próżno, szukać więcej informacji o niej na stronie processing.org) lakoniczny opis znajdziemy pod adresem: Class PMatrix3D Funkcja new w Processing służy do tworzenia nowego obiektu zadanej klasy. Utworzoną macierz można rozumieć jako macierz przekształceń przestrzeni. Przekształcenia te wkrótce nastąpią. Krok 2 Zainicjowanie wektora obrotu (rotationVector) Wektor będzie przechowywał informacje o przesunięciach wskaźnika myszki w płaszczyźnie ekranu X, Y. Świadomie pomijamy tu wartość Z. Nie będzie ona na razie potrzebna i przypisujemy jej na stałe wartość 0. PVector rotationVector=new PVector (0,0,0); // Inicjalizacja obiektu rotationVector klasy PVector (wektor obrotu) PVector jest klasą opisującą dwu lub trójwymiarowy wektor geometryczny znany ze szkoły podstawowej posiadający moduł i kierunek. Podobnie jak obiekt klasy PMatrix3D należy go utworzyć przy wykorzystaniu funkcji new. Krok 3 Aktualizacja wektora obrotu(rotationVector) Wartość X wektora zastąpiona zostaje zmianą położenia myszki wzdłuż osi X Wartość Y wektora zastąpiona zostaje zmianą położenia myszki wzdłuż osi Y Do aktualizacji wektora rotationVector posłuży nam przerwanie obsługujące przeciągnięcie myszki wywoływane funkcją mouseDragged(). Processing przechowuje dwie pomocne zmienne systemowe mouseX oraz pmouseX reprezentujące odpowiednio bieżące oraz poprzednie położenie wskaźnika myszki. Podobnie dzieje się w przypadku współrzędnej Y gdzie zmienne przyjmują oznaczenie mouseY i pmouseY. W naszym przypadku funkcja ma następującą postać: void mouseDragged() { // Funkcja obsługująca przerwanie wywołane przeciągnięciem myszki z wciśniętym przyciskiem rotationVector.x=(pmouseY-mouseY); // Przypisanie współrzędnej X wektora obrotu wartości przesunięcia myszki wzdłuż osi X rotationVector.y=(mouseX-pmouseX); // Przypisanie współrzędnej X wektora obrotu wartości przesunięcia myszki wzdłuż osi Y } Krok 4 Transpozycja macierzy (MATRIX) Zamiana wierszy na kolumny i kolumn na wiersze metodą transpose() klasy PMatrix3D: MATRIX.transpose(); // Transpozycja macierzy 3D Krok 5 Przemnożenie macierzy (MATRIX) przez wektor obrotu (rotationVector) W wyniku mnożenia otrzymujemy nowy, zmodyfikowany wektor obrotu (rotationVector). MATRIX.mult(rotationVector,rotationVector); // Aktualizacja wartości wektora obrotu poprzez przemnożenie go przez macierz przestrzeni Krok 6 Ponowna transpozycja macierzy (MATRIX) Przywrócenie macierzy do pierwotnej formy. Ponieważ pierwsza transpozycja służyła nam wyłącznie do wykonania mnożenia macierzy przez wektor jeszcze raz zamieniamy jej kolumny z wierszami i wiersze z kolumnami. Krok 7 Obrót macierzy współrzędnych (MATRIX) o zadany kąt wokół wektora powstałego ze współrzędnych X,Y,Z wektora obrotu (rotationVector) Kąt ustalony jest arbitralnie (np. 2PI/360*5). Determinuje skalę obrotu w reakcji na ruch myszką. W wyniku obrotu zawartość MATRIX aktualizuje się o nowe wartości. MATRIX.rotate(TWO_PI/360*6,rotationVector.x,rotationVector.y,rotationVector.z); // Obrót macierzy o kąt 6 stopni wokół zmodyfikowanego wektora x,y,z Kąt obrotu będzie decydował o „czułości” myszki. Im większe będzie jej przesunięcie, tym większa będzie jednorazowa skala obrotu obiektu. Krok 8 Transformacja bieżącego układu współrzędnych macierzą MATRIX Wykonujemy mnożenie bieżącego układu współrzędnych (bieżącej macierzy) przez macierz transformacji (MATRIX). Jak ostrzega Processing funkcja jest bardzo powolna, ponieważ próbuje obliczyć odwrotność transformacji. Dla naszego programu jest jednak wystarczająco szybka, teoretycznie można ją też sobie samemu napisać, ale oczywiście zajmie to kolejne linijki kodu. applyMatrix(MATRIX); // Zastąpienie bieżącej przestrzeni 3D przestrzenią przekształconą 3D Krok 9 Reset wektora obrotu Przywrócenie wektora do pierwotnej postaci 0,0,0: rotationVector.set(0,0,0); // Reset wektora obrotu Na tym kończy się algorytm. Oczywiście istnieje wiele różnych implementacji teorii obrotów w praktyce programowania. Ta akurat jest własną metodą autorską. Proponowane, dostępne w sieci, implementacje są zawsze(nie do końca wiadomo dlaczego) dużo bardziej skomplikowane i często nieczytelne. Po zastosowaniu obrotów program przyjmuje następującą formę: PMatrix3D MATRIX=new PMatrix3D // Inicjalizacja obiektu MATRIX klasy PMatrix3D (macierz przestrzeni 3D w ujęciu jednorodnym) (1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1); PVector rotationVector= new PVector (0,0,0); // Inicjalizacja obiektu rotationVector klasy PVector (wektor obrotu) int EARTH_RADIUS=150; // Promień Ziemi int SUN_RADIUS=10; // Promień Słońca int SUN_ORBIT=300; // Promień orbity Słońca; int CELESTIAL_SPHERE_RADIUS=4000; // Promień Sfery Niebieskiej float xSol=0; // Chwilowa współrzędna x środka Śłońca float ySol=0; // Chwilowa współrzędna y środka Śłońca float zSol=0; // Chwilowa współrzędna z środka Śłońca float tSol=0; // Chwilowy azymut Słońca liczony od południka 0 w radianach PShape EARTH; // Deklaracja kształtu przechowującego informacje o kształcie Ziemi PImage EARTH_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Ziemi PShape SUN; // Deklaracja kształtu przechowującego informacje o kształcie Słońca PImage SUN_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Słońca PShape CELESTIAL_SPHERE; // Deklaracja kształtu przechowującego informacje o kształcie Sfery Niebieskiej PImage CELESTIAL_SPHERE_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Sfery Niebieskiej void setup() { size(1800,1000,P3D); // Rozmiar okna 1800/1000 pixeli. Rendering trójwymiarowy frameRate(30); // Ilość klatek na sekundę(ftp/s) background(22); // Tło smooth(4); // Ustawienie poziomu anty-aliasingu sphereDetail(120); // Szczegółowość rysowania sfer EARTH=createShape(SPHERE,EARTH_RADIUS); EARTH_TEXTURE=loadImage("1_earth_8k.jpg"); //EARTH_TEXTURE=loadImage("http://shadedrelief.com/natural3/ne3_data/8192/textures/1_earth_8k.jpg"); SUN=createShape(SPHERE, SUN_RADIUS); SUN_TEXTURE=loadImage("320px-Map_of_the_full_sun.jpg"); //SUN_TEXTURE=loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Map_of_the_full_sun.jpg/320px-Map_of_the_full_sun.jpg"); CELESTIAL_SPHERE=createShape(SPHERE, CELESTIAL_SPHERE_RADIUS); CELESTIAL_SPHERE_TEXTURE=loadImage("eso0932a.jpg"); //CELESTIAL_SPHERE_TEXTURE=loadImage("https://cdn.eso.org/images/large/eso0932a.jpg"); EARTH.setStroke(false); // Brak wyświetlania obrysu EARTH.setTexture(EARTH_TEXTURE); // Przypisanie tekstury kształtowi Ziemi SUN.setStroke(false); // Brak wyświetlania obrysu SUN.setTexture(SUN_TEXTURE); // Przypisanie tekstury kształtowi Słońca CELESTIAL_SPHERE.setStroke(false); // Brak wyświetlania obrysu CELESTIAL_SPHERE.setTexture(CELESTIAL_SPHERE_TEXTURE); // Przypisanie tekstury kształtowi Sfery Niebieskiej } void draw() { background(22); translate(width/2,height/2,0); // Przesunięcie środka układu współrzędnych do centrum ekranu/okna programu MATRIX.transpose(); // Transpozycja macierzy przestrzeni 3D MATRIX.mult(rotationVector,rotationVector); // Aktualizacja wartości wektora obrotu poprzez przemnożenie go przez macierz przestrzeni MATRIX.transpose(); // Ponowna transpozycja macierzy MATRIX.rotate(TWO_PI/360*6,rotationVector.x,rotationVector.y,rotationVector.z); // Obrót macierzy o kąt 6 stopni wokół zmodyfikowanego wektora x,y,z applyMatrix(MATRIX); // Zastąpienie bieżącej przestrzeni 3D przestrzenią przekształconą 3D rotationVector.set(0,0,0); // Reset wektora obrotu rotateX(TWO_PI/4); // Obrót środka układu współrzędnych o 90 stopni wokół osi X rotateZ(TWO_PI/4); // Obrót środka układu współrzędnych o 90 stopni wokół osi Z pushMatrix(); rotateY(TWO_PI/12); rotateZ(TWO_PI/6); shape(CELESTIAL_SPHERE); popMatrix(); directionalLight(255, 255, 210, -xSol, -ySol, -zSol); // Oświetlenie Ziemi światłem jasnożółtym padającym z kierunku Słońca pushMatrix(); // Wepchnięcie macierzy współrzędnych rotateX(-TWO_PI/4); // Obrót środka układu współrzędnych o -90 stopni wokół osi X rotateY(PI); // Obrót środka układu współrzędnych o 180 stopni wokół osi Y ambientLight(28, 28, 28); // Przyciemnienie Ziemi shape(EARTH); // Wyświetlenie obrazu Ziemi lights(); // Reset ustawień funkcji światła popMatrix(); // Wypchnięcie macierzy współrzędnych xSol=SUN_ORBIT*cos(tSol); // Określenie pozycji X środka Słońca ySol=SUN_ORBIT*sin(tSol); // Określenie pozycji Y środka Słońca tSol+=TWO_PI/360; // Ustalenie azymutu Słońca if (tSol>TWO_PI) tSol=0; // Reset azymutu po zakreśleniu przez słońce pełnej orbity pushMatrix(); // Wepchnięcie macierzy współrzędnych translate(xSol,ySol,zSol); // Przesunięcie środka układu współrzędnych do miejsca położenia Słońca ambientLight(255, 255, 210); // Rozświetlenie Słońca shape(SUN); // Wyświetlenie obrazu Słońca lights(); // Reset ustawień funkcji światła popMatrix(); // Wypchnięcie macierzy współrzędnych } void mouseDragged() { // Funkcja obsługująca przerwanie wywołane przeciągnięciem myszki z wciśniętym klawiszem rotationVector.x=(pmouseY-mouseY); // Przypisanie współrzędnej x wektora obrotu wartości przesunięcia myszki wzdłuż osi X rotationVector.y=(pmouseX-mouseX); // Przypisanie współrzędnej y wektora obrotu wartości przesunięcia myszki wzdłuż osi Y } void mouseClicked(MouseEvent event) { // Funkcja obsługująca przerwanie wywołane kliknięciem myszki if (event.getCount() >= 2) MATRIX.reset(); // Przywrócenie Ziemi pierwotnego położenia po dwukrotnym kliknięciu myszką } Na końcu skryptu umieszczona została dodatkowo funkcja obsługująca kliknięcie myszką. Za jej pomocą można, po drobnej modyfikacji, dokonać resetu macierzy układu w wyniku dwukrotnego kliknięcia myszką. Efekt przedstawia się następująco: Do pełni efektu pozornie brakuje jeszcze obsługi ruchu układu względem osi Z np. przy wykorzystaniu ruchu kółka myszki. Tutaj jednak stajemy przed dylematem: czy obrót kółka ma dokonywać obrotu układu względem osi o kąt odpowiadający obrotowi kółka, czy też ma powodować ruch obserwatora wzdłuż osi Z. W zasadzie trzymając się założenia, że operujemy układem, a nie obserwatorem, dylemat brzmi czy ruch kółka powinien obracać układ, czy też powodować jego „puchnięcie” lub „kurczenie”. Kwestia ta pozostawiona została Czytelnikowi do samodzielnego rozstrzygnięcia i ew. implementacji. Można do tego wykorzystać funkcję obsługi przerwania: mouseWheel(). W naszym programie kółko myszki zostanie wykorzystane do innych celów, a ruch obiektu względem osi Z nie zostanie oprogramowany jako, akurat w tym przypadku, zupełnie niepotrzebny. peasycam ver302 Jak zostało podkreślone na wstępie efekt ruchu (obrotu) układu w zależności od ruchu myszki można wywołać poruszając samym układem lub poruszając obserwatorem. W naszym programie zastosujemy na raz obydwie metody, czyli możliwe będzie zarówno poruszanie obiektami jak i przesuwanie obserwującej je kamerą. Zaczynamy od drobnej modyfikacji funkcji przeciągania myszki ograniczając obroty obiektów wyłącznie do prawego klawisza. W ten sposób pozostawiamy lewy klawisz i kółko do swobodnego wykorzystania. Możemy ich użyć na przykład do obrotów kamerą i przesuwania wzdłuż osi X. Oczywiście nie jest to rozwiązanie idealne. Obracanie obiektami prawym przyciskiem, a kamerą lewym przyciskiem może spowodować pogubienie się obserwatora w aktualnej pozycji obiektów na ekranie. Ruch kamery powinien powodować więc korekty mechaniki przesuwania obiektów, aby cały system nie stracił na intuicyjności, sporo do życzenia pozostawia także płynność odwzorowania ruchu myszki na ruch obiektu. To prawda, ale … cyzelować obroty można prawie w nieskończoność, więc w tym przypadku, z miłosierdzia dla Czytelnika, darujemy sobie dalsze, głębsze zmiany programu. void mouseDragged() { // Funkcja obsługująca przerwanie wywołane przeciągnięciem myszki z wciśniętym klawiszem if (mouseButton==RIGHT){ // Ograniczenie obrotów obiektów do prawego klawisza rotationVector.x=(pmouseY-mouseY); // Przypisanie współrzędnej x wektora obrotu wartości przesunięcia myszki wzdłuż osi X rotationVector.y=(pmouseX-mouseX); // Przypisanie współrzędnej y wektora obrotu wartości przesunięcia myszki wzdłuż osi Y } } Teraz już (oddając cząstkę naszej wolności, w zamian za wygodę) możemy zaimplementować do skryptu zewnętrzną bibliotekę peaesycam. Jak twierdzi jej twórca Jonathan Feinberg biblioteka oferuje „a dead-simple” rozwiązanie problemu ruchu kamery za pomocą myszki. Mimo swojej prostoty jest bardzo użytecznym narzędziem, a co więcej nie musimy znać jego konstrukcji, aby z niego skorzystać. Aby użyć funkcji oferowanych przez bibliotekę dołączamy ją do skryptu, deklarujemy obiekt (camera) klasy PeasyCam: import peasy.*; PeasyCam camera; , a następnie w sekcji setup() tworzymy obiekt kamery umiejscowionej w odległości 1000 jednostek od środka układu: camera = new PeasyCam(this, 1000); Gotowe. Teraz już można użyć metod zdefiniowanych dla obiektów tej klasy. Ich lista jest dostępna tutaj: peasycam v302 camera.setMinimumDistance(245); camera.setMaximumDistance(1000); camera.lookAt(width/2, height/2, 0, 500); camera.setRightDragHandler(null); camera.setResetOnDoubleClick(false); Wszystkie ustawienia kamery znajdują się w sekcji setup() skryptu. Nie ma potrzeby umieszczać żadnych komend w sekcji draw(). Ponieważ autor biblioteki zadbał o to, żeby wszystkie nazwy metod były równie śmiertelnie proste, co sama biblioteka, chyba zbędne jest ich omawianie. W skrócie, ustawione zostały kolejno: minimalna i maksymalna odległość kamery od obiektu, punkt na który „patrzy” kamera (pamiętajmy o translacji układu!), wyłączenie prawego przycisku myszy (używany jest do obrotów obiektu, wyłączenie resetu kamery (dwukrotne kliknięcie, jak pamiętamy ma resetować obrót obiektu, a nie kamery). Ostatnią rzeczą jaką musimy zrobić to dodać w funkcji obsługującej dwukrotne kliknięcie myszką reset kamery i korektę współrzędnych obiektu, na który patrzy. camera.reset(); camera.lookAt(width/2, height/2, 0, 500); To wszystko. Zbudowaliśmy trójwymiarowy, geocentryczny model układu Ziemia-Słońce. Nadaliśmy mu tekstury, tło i światło. Wprawiliśmy układ w ruch, a następnie umożliwiliśmy jego obserwację pod dowolnym kątem na dwa niezależnie działające sposoby. Całkiem sporo jak na 100 linijek kodu.  Działający skończony program prezentuje się następująco: Dla zwiększenia czytelności położenia obiektów dodane zostały elipsy i odcinki pozwalające na lepszą orientację w położeniu Ziemi oraz opisy którym przyciskiem myszy przeciągany jest obraz. Samodzielne dodanie tych elementów nie powinno już w tym miejscu nikomu sprawić trudności. Służą do tego komendy ellipse() i line(). Kod programu: import peasy.*; PeasyCam camera; PMatrix3D MATRIX=new PMatrix3D // Inicjalizacja obiektu MATRIX klasy PMatrix3D (macierz przestrzeni 3D w ujęciu jednorodnym) (1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1); PVector rotationVector= new PVector (0,0,0); // Inicjalizacja obiektu rotationVector klasy PVector (wektor obrotu) int EARTH_RADIUS=150; // Promień Ziemi int SUN_RADIUS=10; // Promień Słońca int SUN_ORBIT=300; // Promień orbity Słońca; int CELESTIAL_SPHERE_RADIUS=4000; // Promień Sfery Niebieskiej float xSol=0; // Chwilowa współrzędna x środka Śłońca float ySol=0; // Chwilowa współrzędna y środka Śłońca float zSol=0; // Chwilowa współrzędna z środka Śłońca float tSol=0; // Chwilowy azymut Słońca liczony od południka 0 w radianach PShape EARTH; // Deklaracja kształtu przechowującego informacje o kształcie Ziemi PImage EARTH_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Ziemi PShape SUN; // Deklaracja kształtu przechowującego informacje o kształcie Słońca PImage SUN_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Słońca PShape CELESTIAL_SPHERE; // Deklaracja kształtu przechowującego informacje o kształcie Sfery Niebieskiej PImage CELESTIAL_SPHERE_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Sfery Niebieskiej void setup() { size(1800,1000,P3D); // Rozmiar okna 1800/1000 pixeli. Rendering trójwymiarowy frameRate(30); // Ilość klatek na sekundę(ftp/s) background(22); // Tło smooth(4); // Ustawienie poziomu anty-aliasingu sphereDetail(120); // Szczegółowość rysowania sfer EARTH=createShape(SPHERE,EARTH_RADIUS); EARTH_TEXTURE=loadImage("1_earth_8k.jpg"); //EARTH_TEXTURE=loadImage("http://shadedrelief.com/natural3/ne3_data/8192/textures/1_earth_8k.jpg"); SUN=createShape(SPHERE, SUN_RADIUS); SUN_TEXTURE=loadImage("320px-Map_of_the_full_sun.jpg"); //SUN_TEXTURE=loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Map_of_the_full_sun.jpg/320px-Map_of_the_full_sun.jpg"); CELESTIAL_SPHERE=createShape(SPHERE, CELESTIAL_SPHERE_RADIUS); CELESTIAL_SPHERE_TEXTURE=loadImage("eso0932a.jpg"); //CELESTIAL_SPHERE_TEXTURE=loadImage("https://cdn.eso.org/images/large/eso0932a.jpg"); EARTH.setStroke(false); // Brak wyświetlania obrysu EARTH.setTexture(EARTH_TEXTURE); // Przypisanie tekstury kształtowi Ziemi SUN.setStroke(false); // Brak wyświetlania obrysu SUN.setTexture(SUN_TEXTURE); // Przypisanie tekstury kształtowi Słońca CELESTIAL_SPHERE.setStroke(false); // Brak wyświetlania obrysu CELESTIAL_SPHERE.setTexture(CELESTIAL_SPHERE_TEXTURE); // Przypisanie tekstury kształtowi Sfery Niebieskiej camera = new PeasyCam(this, 1000); camera.setMinimumDistance(245); camera.setMaximumDistance(1000); camera.lookAt(width/2, height/2, 0, 500); camera.setRightDragHandler(null); camera.setResetOnDoubleClick(false); } void draw() { background(22); translate(width/2,height/2,0); // Przesunięcie środka układu współrzędnych do centrum ekranu/okna programu MATRIX.transpose(); // Transpozycja macierzy przestrzeni 3D MATRIX.mult(rotationVector,rotationVector); // Aktualizacja wartości wektora obrotu poprzez przemnożenie go przez macierz przestrzeni MATRIX.transpose(); // Ponowna transpozycja macierzy MATRIX.rotate(TWO_PI/360*6,rotationVector.x,rotationVector.y,rotationVector.z); // Obrót macierzy o kąt 6 stopni wokół zmodyfikowanego wektora x,y,z applyMatrix(MATRIX); // Zastąpienie bieżącej przestrzeni 3D przestrzenią przekształconą 3D rotationVector.set(0,0,0); // Reset wektora obrotu rotateX(TWO_PI/4); // Obrót środka układu współrzędnych o 90 stopni wokół osi X rotateZ(TWO_PI/4); // Obrót środka układu współrzędnych o 90 stopni wokół osi Z pushMatrix(); rotateY(TWO_PI/12); rotateZ(TWO_PI/6); shape(CELESTIAL_SPHERE); popMatrix(); directionalLight(255, 255, 210, -xSol, -ySol, -zSol); // Oświetlenie Ziemi światłem jasnożółtym padającym z kierunku Słońca pushMatrix(); // Wepchnięcie macierzy współrzędnych rotateX(-TWO_PI/4); // Obrót środka układu współrzędnych o -90 stopni wokół osi X rotateY(PI); // Obrót środka układu współrzędnych o 180 stopni wokół osi Y ambientLight(28, 28, 28); // Przyciemnienie Ziemi shape(EARTH); // Wyświetlenie obrazu Ziemi lights(); // Reset ustawień funkcji światła popMatrix(); // Wypchnięcie macierzy współrzędnych xSol=SUN_ORBIT*cos(tSol); // Określenie pozycji X środka Słońca ySol=SUN_ORBIT*sin(tSol); // Określenie pozycji Y środka Słońca tSol+=TWO_PI/360; // Ustalenie azymutu Słońca if (tSol>TWO_PI) tSol=0; // Reset azymutu po zakreśleniu przez słońce pełnej orbity pushMatrix(); // Wepchnięcie macierzy współrzędnych translate(xSol,ySol,zSol); // Przesunięcie środka układu współrzędnych do miejsca położenia Słońca ambientLight(255, 255, 210); // Rozświetlenie Słońca shape(SUN); // Wyświetlenie obrazu Słońca lights(); // Reset ustawień funkcji światła popMatrix(); // Wypchnięcie macierzy współrzędnych } void mouseDragged() { // Funkcja obsługująca przerwanie wywołane przeciągnięciem myszki z wciśniętym klawiszem if (mouseButton==RIGHT){ // Ograniczenie obrotów obiektów do prawego klawisza rotationVector.x=(pmouseY-mouseY); // Przypisanie współrzędnej x wektora obrotu wartości przesunięcia myszki wzdłuż osi X rotationVector.y=(pmouseX-mouseX); // Przypisanie współrzędnej y wektora obrotu wartości przesunięcia myszki wzdłuż osi Y } } void mouseClicked(MouseEvent event) { // Funkcja obsługująca przerwanie wywołane kliknięciem myszki if (event.getCount() == 2) { MATRIX.reset(); // Przywrócenie Ziemi pierwotnego położenia po dwukrotnym kliknięciu myszką camera.reset(); camera.lookAt(width/2, height/2, 0, 500); } } Od Autora Dziękuję wszystkim, którzy dotarli aż do tego miejsca. Jestem pod wrażeniem Mam nadzieję, że temat okazał się ciekawy. Przepraszam przy okazji za wszystkie niezręczności w tekście, są praktycznie nieuniknione. Pojawia się pytanie co dalej? Tutaj ograniczeniem jest tylko pojemność kory mózgowej. Można dalej poprawiać program (jest na pewno wieele do ulepszenia, choć przyznam, że jest to najczystszy kod jaki mi osobiście kiedykolwiek udało się napisać, niestety zwykle przypomina śmietnik), można podjąć próbę zasymulowania innego układu po swojemu, można też dodać kolejne obiekty, wyposażyć w działka laserowe, oprogramować kolizje (w sumie ciekawy temat na ew. nowe artykuły) i zaprosić znajomych do własnej kosmicznej strzelaniny (biblioteka: network) w stylu "World of..", a potem ładne to wszystko ubrać i zarobić swój pierwszy 1 000 000$ Czego wszystkim Czytelnikom FORBOTa serdecznie życzę!
  3. PImage oraz rotateX(), rotate(Y), rotateZ() Nie ulega wątpliwości, że wyobrażenie sobie dwóch kół jako Słońca i Ziemi wymaga sporej wyobraźni. Nadszedł czas, aby temu zaradzić. W pierwszej kolejności obiekty zostaną wyposażone w odpowiednie tekstury. W tym celu wykorzystany zostanie obiekt typu PImage. Podobnie jak PShape, PImage jest typem danych. Jednak w odróżnieniu od PShape, przechowuje on informacje nie o kształtach, ale o obrazie. Dostępne formaty obrazu to .gif, .jpg, .tga oraz .png. W pierwszej kolejności deklarujemy obiekt przechowujący teksturę Ziemi (EARTH_TEXTURE); PImage EARTH_TEXTURE; // Deklaracja obiektu przechowującego informacje o teksturze Ziemi Następnie, przypisujemy mu obraz który, znowu, podobnie jak w przypadku PShape, może być wczytany z pliku lub stworzony od początku za pośrednictwem metod: loadImage() lub createImage(). W tym przypadku, gotowy obraz zostanie wczytany. Jeśli nie podamy wprost lokalizacji pliku Processing będzie poszukiwał go bezpośrednio w folderze, w którym umieszczony jest skrypt programu lub w podkatalogu DATA, również znajdującym się w tym folderze, o ile zostanie taki wcześniej stworzony. Możliwe jest jednak literalne podanie ścieżki dostępu do pliku znajdującego się w innym miejscu na komputerze lub wskazanie adresu, jeśli obraz pobierany jest z Internetu. Obraz Ziemi w naszym programie może zostać pobrany albo bezpośrednio ze strony Tom Patterson, www.shadedrelief.com -zawierającej m.in. interesujące tekstury naszego globu- albo z pliku ściągniętego na dysk (osobiście zdecydowanie zachęcam do ściągania plików na własny dysk). Należy pamiętać, że pobieranie danych z sieci oprócz oczywiście konieczności dostępu do Internetu może wiązać się ze znacznymi opóźnieniami wczytywania danych (a nawet uporczywymi błędami i zatrzymaniem programu) oraz niedostosowaniem jego wymiaru do naszych oczekiwań. Grozi również naruszeniem praw autorskich. Wszystkie tekstury użyte w tym programie nie są objęte licencjami komercyjnymi i mogą być swobodnie wykorzystywane. W treści kodu zostały podane zakomentowane komendy odwołujące się do pliku znajdującego się w Internecie oraz działające komendy odwołujące się do pliku wcześniej ściągniętego. Załadowanie obrazu do obiektu EARTH_TEXTURE zrealizowane zostało następującą komendą: EARTH_TEXTURE=loadImage("1_earth_8k.jpg"); //Alternatywnie: EARTH_TEXTURE=loadImage("http://shadedrelief.com/natural3/ne3_data/8192/textures/1_earth_8k.jpg"); Na koniec konieczne jest przypisanie tekstury przechowywanej przez obiekt typu PImage (EARTH_TEXTURE) obiektowi typu PShape (EARTH). Służy do tego metoda setTexture(): EARTH.setTexture(EARTH_TEXTURE); // Przypisanie tekstury kształtowi Jesteśmy gotowi do wykonania kolejnej wersji programu: Niestety, efekt nie jest zadowalający. Po pierwsze, Ziemia jest nienaturalnie niebieska. Po drugie, Słońce krąży wokół zerowego południka zamiast wokół równika. Na szczęście, łatwo to zmienić. Nadmiar koloru niebieskiego spowodowany jest wcześniejszym użyciem metody setFill() do ustawienia koloru wypełniania kształtu. W zasadzie po załadowaniu tekstury nie jest już to potrzebne, chyba że ma to na celu osiągnięcie specjalnego efektu. W naszym programie usuwamy wypełnienie kształtu jako zbędne, kasując odpowiednią linię kodu. Rozwiązanie drugiego problemu będzie wymagało od nas podjęcia decyzji co do początkowej pozycji Ziemi widocznej na ekranie oraz płaszczyzny w jakiej ma się wokół niej poruszać Słońce. Pamiętamy, że chcemy, aby Słońce poruszało się po okręgu w płaszczyźnie równika. Najlepiej, aby swój ruch zaczynało na południku zerowym, który jednocześnie powinien przechodzić przez środek okna (innymi słowy chcemy, aby Słońce w momencie początku swojego obiegu znajdowało się dokładnie w centrum okna i poruszało się w poziomie). Jednocześnie Ziemia ma być zwrócona do nas widokiem Europy i Afryki tak, abyśmy mogli obserwować południk, w którym Słońce zaczyna swój bieg. Zacznijmy od zmiany płaszczyzny obrotu Słońca. Posłuży do tego grupa funkcji pokrewnych znanej już funkcji translate(), wywoływanych komendami: rotateX(), rotateY() oraz rotateZ(). Funkcje pozwalają na dokonanie obrotu układu współrzędnych wokół zadanej osi X, Y, lub Z o zadany kąt wyrażony w radianach w kierunku zgodnym z ruchem wskazówek zegara (wartości dodatnie) lub przeciwnym (wartości ujemne). Należy pamiętać, że wykonywane kolejno obroty się kumulują. Aby Słońce poruszało się zgodnie z dokonanym wyborem, konieczne jest dokonanie dodatkowych przekształceń układu współrzędnych polegające na jego obrocie o 90 stopni wokół osi X i o tyle samo wokół osi Z. Początkowe ustawienia układu współrzędnych będą więc następujące: translate(width/2,height/2,0); // Przesunięcie środka układu współrzędnych do centrum ekranu/okna programu rotateX(TWO_PI/4); // Obrót środka układu współrzędnych o 90 stopni wokół osi X rotateZ(TWO_PI/4); // Obrót środka układu współrzędnych o 90 stopni wokół osi Z Po dodaniu nowych funkcji Słońce będzie poruszało się już zgodnie z przyjętymi założeniami. Przy okazji przekształceń układu współrzędnych przesunięciu uległo nie tylko Słońce, ale i Ziemia. Pytanie, jak obrócić Ziemię, aby przy okazji nie zmieniać położenia Słońca? Bardzo prosto! Oczywiście, przy pomocy pushMatrix(). Ponieważ znajdujemy się gdzieś na środku Pacyfiku powinniśmy obrócić Ziemię o -90 stopni wokół osi X oraz 180 stopni wokół osi Y. Obroty jednak muszą być dokonywane poza układem współrzędnych, w których znajduje się Słońce. W konsekwencji część kodu odpowiedzialna za wyświetlenie Ziemi powinna wyglądać następująco: pushMatrix(); // Wepchnięcie macierzy współrzędnych rotateX(-TWO_PI/4); // Obrót środka układu współrzędnych o -90 stopni wokół osi X rotateY(PI); // Obrót środka układu współrzędnych o 180 stopni wokół osi Y shape(EARTH); // Rysunek Ziemi popMatrix(); // Wypchnięcie macierzy współrzędnych Na koniec w ten sam sposób jak w przypadku tekstury Ziemi możemy przypisać teksturę Słońcu. Wykorzystany w programie obraz pochodzi ze strony: https://pl.m.wikipedia.org Dzięki wprowadzonym zmianom program działa już w zadowalający sposób: Odpowiada za niego następujący kod: int EARTH_RADIUS=150; // Promień Ziemi int SUN_RADIUS=10; // Promień Słońca int SUN_ORBIT=300; // Promień orbity Słońca; float xSol=0; // Chwilowa współrzędna x środka Śłońca float ySol=0; // Chwilowa współrzędna y środka Śłońca float zSol=0; // Chwilowa współrzędna z środka Śłońca float tSol=0; // Chwilowy azymut Słońca liczony od południka 0 w radianach PShape EARTH; // Deklaracja kształtu przechowującego informacje o kształcie Ziemi PImage EARTH_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Ziemi PShape SUN; // Deklaracja kształtu przechowującego informacje o kształcie Słońca PImage SUN_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Słońca void setup() { size(1800,1000,P3D); // Rozmiar okna 1800/1000 pixeli. Rendering trójwymiarowy frameRate(30); // Ilość klatek na sekundę(ftp/s) background(22); // Tło smooth(4); // Ustawienie poziomu anty-aliasingu sphereDetail(120); // Szczegółowość rysowania sfer EARTH=createShape(SPHERE,EARTH_RADIUS); EARTH_TEXTURE=loadImage("1_earth_8k.jpg"); // Alternatywnie: EARTH_TEXTURE=loadImage("http://shadedrelief.com/natural3/ne3_data/8192/textures/1_earth_8k.jpg"); SUN=createShape(SPHERE, SUN_RADIUS); SUN_TEXTURE=loadImage("320px-Map_of_the_full_sun.jpg"); // Alternatywnie: SUN_TEXTURE=loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Map_of_the_full_sun.jpg/320px-Map_of_the_full_sun.jpg"); EARTH.setStroke(false); // Brak wyświetlania obrysu EARTH.setTexture(EARTH_TEXTURE); // Przypisanie tekstury kształtowi Ziemi SUN.setStroke(false); // Brak wyświetlania obrysu SUN.setTexture(SUN_TEXTURE); // Przypisanie tekstury kształtowi Słońca } void draw() { background(22); // Wymazanie klatki translate(width/2,height/2,0); // Przesunięcie środka układu współrzędnych do centrum rotateX(TWO_PI/4); // Obrót środka układu współrzędnych o 90 stopni wokół osi X rotateZ(TWO_PI/4); // Obrót środka układu współrzędnych o 90 stopni wokół osi Z pushMatrix(); // Wepchnięcie macierzy współrzędnych rotateX(-TWO_PI/4); // Obrót środka układu współrzędnych o -90 stopni wokół osi X rotateY(PI); // Obrót środka układu współrzędnych o 180 stopni wokół osi Y shape(EARTH); // Rysunek Ziemi popMatrix(); // Wypchnięcie macierzy współrzędnych xSol=SUN_ORBIT*cos(tSol); // Określenie pozycji pozycji X środka Słońca ySol=SUN_ORBIT*sin(tSol); // Określenie pozycji pozycji Y środka Słońca tSol+=TWO_PI/360; // Ustalenie azymutu Słońca if (tSol>TWO_PI) tSol=0; // Reset azymutu po zakreśleniu przez słońce całej orbity pushMatrix(); // Wepchnięcie macierzy współrzędnych translate(xSol,ySol,zSol); // Przesunięcie środka układu współżędnych do miejsca położenia Słońca shape(SUN); // Wyświetlenie obrazu Słońca popMatrix(); // Wypchnięcie macierzy współrzędnych } Efekt jest przyzwoity, ale czegoś w nim brakuje… Ależ, tak! Światła i cieni! ambientLight(), directionalLight() i lights() Po uporaniu się z mechaniką obrotu Słońca wokół Ziemi oraz określeniem ich wzajemnej pozycji (oczywiście przymykając oko na zastosowane grube uproszczenia – naprawdę nie warto akurat przy tej okazji, zagłębiać się w matematyczne niuanse długości perycentrum i jej przekształceń na system geocentryczny) nadszedł czas na dodanie do modelu nieco ożywiającego go światła. Jak można się domyślać Processing oferuje naprawdę długą listę funkcji operowania światłem. W programie skorzystano tylko z kilku z nich. Patrząc na aktualny wygląd modelu, rzuca się w oczy brak zaakcentowania dnia i nocy. Słońce nie oświetla Ziemi, a tylko wokół niej krąży. Również sam wygląd Ziemi pozostawia wiele do życzenia. Jej światło jest płaskie, dwuwymiarowe i mało rzeczywiste. Ciekawym zabiegiem byłoby dodanie do obrazu Ziemi obrazu jej atmosfery. Pozostawiam to już jednak, zainteresowanemu Czytelnikowi. Można to zrobić na przykład zamykając obraz sfery Ziemi w obrazie drugiej sfery o odpowiednio dobranym parametrze alpha i dopiero wtedy zastosować wybrane funkcje oświetlenia. Na koniec sam obraz Słońca. Jest zbyt ponury. Teksturę należy rozświetlić. Zacznijmy od nadania jasności Słońcu. W tym celu można wykorzystać funkcję ambientLight(). Funkcja prawie zawsze występuje z innymi funkcjami oświetlenia. W tym programie występuje jednak samotnie, jako wygodne narzędzie rozjaśnienia Słońca. ambientLight(255, 255, 210); // Rozświetlenie Słońca Funkcja nie przyjmuje kolorów w zapisie heksadecymalnym. W zależności od wybranego trybu pracy z kolorami konieczne jest podanie wartość RGB lub HSB. Zadaniem funkcji jest równomierne oświetlenie obiektu rozproszonym światłem o zadanym kolorze. Ponieważ Ziemia będzie oświetlana w inny sposób niż Słońce, konieczne jest zadbanie, aby po rozświetleniu Słońca nie zostały rozświetlone rozproszonym jasnożółtym kolorem również inne obiekty na ekranie. Funkcja lights() może posłużyć do przywrócenia wszystkim pozostałym funkcjom światła ustawień domyślnych. Została zatem umieszczona bezpośrednio po wywołaniu Słońca. Wywołanie obrazu Słońca przyjmie więc ostatecznie następującą formę: pushMatrix(); // Wepchnięcie macierzy współrzędnych translate(xSol,ySol,zSol); // Przesunięcie środka układu współrzędnych do miejsca położenia Słońca ambientLight(255, 255, 210); // Rozświetlenie Słońca shape(SUN); // Wyświetlenie obrazu Słońca lights(); // Reset ustawień funkcji światła popMatrix(); // Wypchnięcie macierzy współrzędnych Podobnie jak Słońcu należy nadać nieco rozproszonego światła Ziemi, głównie w celu podkreślenia różnicy pomiędzy nocą, a dniem, po oświetleniu Ziemi światłem kierunkowym pochodzącym ze Słońca. Bez tego zabiegu, obraz Ziemi w nocy byłby zdecydowanie zbyt ciemny (praktycznie czarny). Wywołanie obrazu Ziemi w świetle rozproszonym będzie wyglądało następująco: pushMatrix(); // Wepchnięcie macierzy współrzędnych rotateX(-TWO_PI/4); // Obrót środka układu współrzędnych o -90 stopni wokół osi X rotateY(PI); // Obrót środka układu współrzędnych o 180 stopni wokół osi Y ambientLight(28, 28, 28); // Przyciemnienie Ziemi shape(EARTH); // Wyświetlenie obrazu Ziemi lights(); // Reset ustawień funkcji światła popMatrix(); // Wypchnięcie macierzy współrzędnych Do wywołania efektu dnia i nocy w programie zostanie wykorzystana funkcja directionalLight(). Pozwala ona na emitowanie światła z konkretnego, zadanego kierunku. Natężenie światła jest tym większe im bardziej prostopadła do jego kierunku jest powierzchnia, na którą pada. Podobnie jak w przypadku ambientLight() funkcja przyjmuje tylko kolory wyrażone w RCB lub HSB. W naszym programie funkcji nie zamykamy w nawiasach pushMatrix()-popMatrix(). Jest ona wykonywana z poziomu głównego układu współrzędnych. directionalLight(255, 255, 210, -xSol, -ySol, -zSol); // Oświetlenie Ziemi światłem jasnożółtym padającym ze Słońca Należy zwrócić uwagę, że zadeklarowane w funkcji „współrzędne” Słońca przyjmują wartości ujemne. Dzieje się tak dlatego, że w rzeczywistości nie są to współrzędne, a wektory padania światła, skierowane z jego źródła w kierunku rozświetlanego obiektu. Na koniec, grafitowe do tej pory tło warto byłoby zastąpić bardziej atrakcyjnym obrazem. Ciekawym rozwiązaniem byłoby dodanie do Słońca innych obiektów Sfery Niebieskiej i umieszczenie obserwatora pomiędzy Ziemią a Sferą. Pomimo, że efekt byłby interesujący, stracilibyśmy jednak możliwość oglądania modelu z obecnej perspektywy. Dlatego w programie przyjęto, że umowna sfera rozciąga się 4000 jednostek od Ziemi i jest podobnie jak Ziemia nieruchoma. Jest to więc raczej teatralne tło niż element modelu. Sfera niebieska jest kolejnym trzecim już obiektem PShape, a jego tworzenie i wyświetlanie nie odbiega od tego w jaki sposób wyświetlone zostały Ziemia i Słońce. Do kształtu sfery przypisany został obraz Drogi Mlecznej pochodzący ze strony https://www.eso.org Ostateczny efekt jaki uzyskaliśmy zasługuje na coś więcej niż schematyczne .gify. Wygląda już całkiem nieźle i to przy minimalnych nakładach pracy. Kod programu: int EARTH_RADIUS=150; // Promień Ziemi int SUN_RADIUS=10; // Promień Słońca int SUN_ORBIT=300; // Promień orbity Słońca; int CELESTIAL_SPHERE_RADIUS=4000; // Promień Sfery Niebieskiej float xSol=0; // Chwilowa współrzędna x środka Śłońca float ySol=0; // Chwilowa współrzędna y środka Śłońca float zSol=0; // Chwilowa współrzędna z środka Śłońca float tSol=0; // Chwilowy azymut Słońca liczony od południka 0 w radianach PShape EARTH; // Deklaracja kształtu przechowującego informacje o kształcie Ziemi PImage EARTH_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Ziemi PShape SUN; // Deklaracja kształtu przechowującego informacje o kształcie Słońca PImage SUN_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Słońca PShape CELESTIAL_SPHERE; // Deklaracja kształtu przechowującego informacje o kształcie Sfery Niebieskiej PImage CELESTIAL_SPHERE_TEXTURE; // Deklaracja obrazu przechowującego informacje o teksturze Sfery Niebieskiej void setup() { size(1800,1000,P3D); // Rozmiar okna 1800/1000 pixeli. Rendering trójwymiarowy frameRate(30); // Ilość klatek na sekundę(ftp/s) background(22); // Tło smooth(4); // Ustawienie poziomu anty-aliasingu sphereDetail(120); // Szczegółowość rysowania sfer EARTH=createShape(SPHERE,EARTH_RADIUS); EARTH_TEXTURE=loadImage("1_earth_8k.jpg"); //EARTH_TEXTURE=loadImage("http://shadedrelief.com/natural3/ne3_data/8192/textures/1_earth_8k.jpg"); SUN=createShape(SPHERE, SUN_RADIUS); SUN_TEXTURE=loadImage("320px-Map_of_the_full_sun.jpg"); //SUN_TEXTURE=loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Map_of_the_full_sun.jpg/320px-Map_of_the_full_sun.jpg"); CELESTIAL_SPHERE=createShape(SPHERE, CELESTIAL_SPHERE_RADIUS); CELESTIAL_SPHERE_TEXTURE=loadImage("eso0932a.jpg"); //CELESTIAL_SPHERE_TEXTURE=loadImage("https://cdn.eso.org/images/large/eso0932a.jpg"); EARTH.setStroke(false); // Brak wyświetlania obrysu EARTH.setTexture(EARTH_TEXTURE); // Przypisanie tekstury kształtowi Ziemi SUN.setStroke(false); // Brak wyświetlania obrysu SUN.setTexture(SUN_TEXTURE); // Przypisanie tekstury kształtowi Słońca CELESTIAL_SPHERE.setStroke(false); // Brak wyświetlania obrysu CELESTIAL_SPHERE.setTexture(CELESTIAL_SPHERE_TEXTURE); // Przypisanie tekstury kształtowi Sfery Niebieskiej } void draw() { background(22); shape(CELESTIAL_SPHERE); translate(width/2,height/2,0); // Przesunięcie środka układu współrzędnych do centrum ekranu/okna programu rotateX(TWO_PI/4); // Obrót środka układu współrzędnych o 90 stopni wokół osi X rotateZ(TWO_PI/4); // Obrót środka układu współrzędnych o 90 stopni wokół osi Z directionalLight(255, 255, 210, -xSol, -ySol, -zSol); // Oświetlenie Ziemi światłem jasnożółtym padającym z kierunku Słońca pushMatrix(); // Wepchnięcie macierzy współrzędnych rotateX(-TWO_PI/4); // Obrót środka układu współrzędnych o -90 stopni wokół osi X rotateY(PI); // Obrót środka układu współrzędnych o 180 stopni wokół osi Y ambientLight(28, 28, 28); // Przyciemnienie Ziemi shape(EARTH); // Wyświetlenie obrazu Ziemi lights(); // Reset ustawień funkcji światła popMatrix(); // Wypchnięcie macierzy współrzędnych xSol=SUN_ORBIT*cos(tSol); // Określenie pozycji pozycji X środka Słońca ySol=SUN_ORBIT*sin(tSol); // Określenie pozycji pozycji Y środka Słońca tSol+=TWO_PI/360; // Ustalenie azymutu Słońca if (tSol>TWO_PI) tSol=0; // Reset azymutu po zakreśleniu przez słońce pełnej orbity pushMatrix(); // Wepchnięcie macierzy współrzędnych translate(xSol,ySol,zSol); // Przesunięcie środka układu współrzędnych do miejsca położenia Słońca ambientLight(255, 255, 210); // Rozświetlenie Słońca shape(SUN); // Wyświetlenie obrazu Słońca lights(); // Reset ustawień funkcji światła popMatrix(); // Wypchnięcie macierzy współrzędnych } Na tym kończy się część trzecia. Zachęcam do eksperymentów z teksturami, światłem, rodzajami predefiniowanych i własnych obiektów, mechaniką ich wzajemnego ruchu oraz tłem. To wciąga! W części #4 postaramy się jeszcze bardziej uatrakcyjnić scenę poruszając w inny sposób obiektami oraz samym obserwatorem. Być może wystarczy też miejsca na wprowadzenie obiektów dwuwymiarowych symulujących trójwymiarowe. Szybkość pojawienia się kolejnej części zależy od zainteresowania tematem wyrażonym ilością łapek i komentarzy. Żartuję. Część czwarta już czeka na edycję.
  4. pushMatrix(), popMatrix() Czas na dodanie do programu obrazu Słońca. W poprzedniej części artykułu przedstawione zostały zasady tworzenia obiektów typu PShape. Tworzenie obrazu Słońca dokonywane będzie dokładnie w ten sam sposób co tworzenie obrazu Ziemi. Jedyna różnica polegała będzie na różnicach położenia tych obiektów w układzie współrzędnych. Co więcej, o ile położenie Ziemi będzie stałe, o tyle położenie Słońca będzie się zmieniało w czasie, ponieważ będzie ono krążyło bezustannie wokół Ziemi, a w naszym programie w każdej klatce wyświetlane w nieco innym miejscu. Oczywiście istnieje wiele metod umożliwiających realizację tego zadania, jednak Processing oferuje niezwykle wygodną funkcję pushMatrix(), która w połączeniu ze znanym już translate() znacznie je ułatwia. pushMatrix() zapisuje bieżącą macierz transformacji na stosie danych, a popMatrix() ponownie go stamtąd wydobywa. Transformacje wykonane pomiędzy pushMatrix() a popMatrix() będą dotyczyły układu znajdującego się na stosie. Upraszczając, działanie funkcji pushMatrix() możemy wyobrazić sobie jako wciśnięcie układu współrzędnych razem ze znajdującymi się w nim obiektami w inną rzeczywistość, niż ta która dokonuje się na ekranie. W trakcie przebywania w tej innej rzeczywistości możliwe jest wykonywanie translacji układu współrzędnych oraz operacji na „wciśniętych” razem z nim obiektach. Wywołanie popMatrix() powoduje powrót do poprzedniego układu jednak obiekty pojawiają się w miejscach w jakich znalazły się po dokonaniu translacji poza układem. Dla przykładu narysowanie sfery, a następnie wywołanie funkcji pushMatrix() oraz przesunięcie sfery o 10 jednostek zakończone wywołaniem popMatrix() spowoduje pojawienie się na ekranie obrazu dwóch sfer przesuniętych względem siebie o 10 jednostek, pomimo, że obrazy dotyczą tej samej sfery. Potęga i jednocześnie prostota pushMatrix() jest porównywalna do translate(). Połączenie ich razem jest nieocenionym narzędziem programistycznym. Pojawia się pytanie, gdzie zostanie wyświetlone „Słońce” jeśli nie zostanie przesunięte w odpowiednie miejsce? Oczywiście wewnątrz „Ziemi”, tj. w środku układu współrzędnych. Ponieważ jego promień będzie mniejszy od ziemskiego, nie będzie nawet widoczne. Konieczne jest zatem jego przesunięcie w stosunku do środka współrzędnych oraz wprawienie w ruch wokół Ziemi. Zanim program zostanie odpowiednio zmodyfikowany warto rzucić okiem na stan aktualny, czyli z obrazem Słońca: int EARTH_RADIUS=150; // Promień Ziemi int SUN_RADIUS=10; // Promień Słońca PShape EARTH; // Deklaracja kształtu przechowującego informacje o kształcie Ziemii PShape SUN; // Deklaracja kształtu przechowującego informacje o kształcie Słońca void setup() { size(1800,1000,P3D); // Rozmiar okna 1800/1000 pixeli. Rendering trójwymiarowy frameRate(30); // Ilość klatek na sekundę(ftp/s) background(22); // Tło smooth(4); // Ustawienie poziomu anty-aliasingu sphereDetail(120); // Szczegółowość rysowania sfer EARTH=createShape(SPHERE,EARTH_RADIUS); SUN=createShape(SPHERE, SUN_RADIUS); EARTH.setStroke(false); // Brak wyświetlania obrysu EARTH.setFill(#0D8FBC); // Kolor wypełnienia #0D8FBC (błękitny) SUN.setStroke(false); // Brak wyświetlania obrysu SUN.setFill(#F5E20A); // Kolor wypełnienia #F5E20A (żółty) } void draw() { translate(width/2,height/2,0); // Przesunięcie środka układu współrzędnych do centrum ekranu/okna programu shape(EARTH); // Rysunek Ziemi shape(SUN); // Rysunek Słońca } Z punktu widzenia obserwatora na Ziemi Słońce porusza się po nieboskłonie idealnym ruchem okrężnym. Peryhelium i aphelium orbity widoczne może być jako pozorne zmniejszenie lub zwiększenie promienia Słońca, co gołym okiem jest jednak praktycznie niemożliwe. Dlatego w programie przyjęto, że ruch Słońca wokół Ziemi odbywa się po okręgu i nie jest dokonywana korekta promienia gwiazdy. Związane jest to również z faktem, że skala i cykl tej korekty wymaga dość skomplikowanych obliczeń oraz wprowadzania pojęć, które zamieniłyby ten artykuł (i program) w rozprawkę z dziedziny astronomii i astronawigacji. Uwzględniając powyższe ograniczenia, do określenia pozycji słońca względem Ziemi potrzebne będą następujące zmienne: float xSol=0; // Chwilowa współrzędna x środka Słońca float ySol=0; // Chwilowa współrzędna y środka Słońca float zSol=0; // Chwilowa współrzędna z środka Słońca float tSol=0; // Chwilowy azymut Słońca liczony od południka 0 w stopniach int SUN_ORBIT=600; // Promień orbity Słońca Przez chwilowe współrzędne środka słońca należy rozumieć położenie obrazu Słońca w oknie programu w trakcie wyświetlania kolejnych klatek. Chwilowy azymut określa miejsce w jakim znajduje się obraz Słońca względem obrazu ziemskich południków. Współrzędne środka Słońca wyliczane są na podstawie równania okręgu zgodnie z następującym algorytmem: xSol=SUN_ORBIT*cos(tSol); // wyliczenie pozycji x Słońca ySol=SUN_ORBIT*sin(tSol); // wyliczenie pozycji y Słońca tSol+=TWO_PI/360; // inkrementacja azymutu o 1 stopień na klatkę if (tSol>TWO_PI) tSol=0; // reset azymutu po zakreśleniu przez Słońce całej orbity Mając wszystkie niezbędne dane możliwe jest dokonanie odpowiednich przekształceń wykorzystując funkcje popMatrix() oraz translate() pushMatrix(); // wepchnięcie macierzy współrzędnych translate(xSol,ySol,zSol); // przesunięcie środka układu shape(SUN); // wyświetlenie obrazu Słońca popMatrix(); // wypchnięcie macierzy Jak łatwo zauważyć nie została obliczona wartość zmiennej zSol. Nie jest to na tym etapie niezbędne. Ruch Słońca względem osi Z w układzie geocentrycznym wywoływany jest przez nachylenie ekliptyki. Jego określenie wymaga odrębnych, dość zawiłych obliczeń, które nie wnoszą jednak niczego nowego w zakresie opisywanych funkcji, zmniejszając jednocześnie przejrzystość kodu. Tym samym w naszym programie Słońce okrąża Ziemię idealnie w płaszczyźnie równika. Działanie programu uwzględniającego nachylenie ekliptyki zostanie przedstawione na końcu artykułu jako dodatek. Po uzupełnieniu programu o komendy definiujące pozycję Słońca oraz zasady jego wyświetlania pełny kod będzie wyglądał następująco: int EARTH_RADIUS=150; // Promień Ziemi int SUN_RADIUS=10; // Promień Słońca int SUN_ORBIT=300; // Promień orbity Słońca; float xSol=0; // Chwilowa współrzędna x środka Śłońca float ySol=0; // Chwilowa współrzędna y środka Śłońca float zSol=0; // Chwilowa współrzędna z środka Śłońca float tSol=0; // Chwilowy azymut Słońca liczony od południka 0 w radianach float tSolFromSeasonStart=0; // Suma azymutów Słońca w sezonie w okresie obserwacji w radianach PShape EARTH; // Deklaracja kształtu przechowującego informacje o kształcie Ziemii PShape SUN; // Deklaracja kształtu przechowującego informacje o kształcie Słońca void setup() { size(1800,1000,P3D); // Rozmiar okna 1800/1000 pixeli. Rendering trójwymiarowy frameRate(30); // Ilość klatek na sekundę(ftp/s) background(22); // Tło smooth(4); // Ustawienie poziomu anty-aliasingu sphereDetail(120); // Szczegółowość rysowania sfer EARTH=createShape(SPHERE,EARTH_RADIUS); SUN=createShape(SPHERE, SUN_RADIUS); EARTH.setStroke(false); // Brak wyświetlania obrysu EARTH.setFill(#0D8FBC); // Kolor wypełnienia #0D8FBC (błękitny) SUN.setStroke(false); // Brak wyświetlania obrysu SUN.setFill(#F5E20A); // Kolor wypełnienia #F5E20A (żółty) } void draw() { background(22); // Wymazanie obrazu klatki translate(width/2,height/2,0); // Przesunięcie środka układu współrzędnych do centrum ekranu/okna programu shape(EARTH); // Rysunek Ziemi xSol=SUN_ORBIT*cos(tSol); ySol=SUN_ORBIT*sin(tSol); tSol+=TWO_PI/360; if (tSol>TWO_PI) tSol=0; pushMatrix(); translate(xSol,ySol,zSol); shape(SUN); popMatrix(); } Po uruchomieniu programu w tej formie otrzymamy następujący obraz: Efekt niesie niemiłą niespodziankę! Słońce zamiast okrążać Ziemię rysuje wokół niej okrąg. Jak już wspomniano, spowodowane jest to faktem, że wpychanie i wypychanie macierzy nie powoduje przesunięcia lub wymazania obrazu obiektów sprzed tej operacji. Widzimy po prostu kolejne obrazy tego samego obiektu, którego przesunięcia odbywają się poza wyświetlaną macierzą współrzędnych. Jedyny sposób, aby temu zaradzić to wyczyszczenie klatki i przerysowanie jej na nowo. Z pomocą przychodzi tu funkcja background(). Jej zastosowanie w pętli draw() będzie powodowało wymazywanie zawartości każdej kolejnej wyświetlanej klatki i rozwiąże problem. background(22); // Wymazanie obrazu klatki Po uzupełnieniu programu o powyższą komendę działa on już zgodnie z oczekiwaniami: Ostatecznie kod przyjmuje postać: int EARTH_RADIUS=150; // Promień Ziemi int SUN_RADIUS=10; // Promień Słońca int SUN_ORBIT=300; // Promień orbity Słońca; float xSol=0; // Chwilowa współrzędna x środka Śłońca float ySol=0; // Chwilowa współrzędna y środka Śłońca float zSol=0; // Chwilowa współrzędna z środka Śłońca float tSol=0; // Chwilowy azymut Słońca liczony od południka 0 w radianach float tSolFromSeasonStart=0; // Suma azymutów Słońca w sezonie w okresie obserwacji w radianach PShape EARTH; // Deklaracja kształtu przechowującego informacje o kształcie Ziemii PShape SUN; // Deklaracja kształtu przechowującego informacje o kształcie Słońca void setup() { size(1800,1000,P3D); // Rozmiar okna 1800/1000 pixeli. Rendering trójwymiarowy frameRate(30); // Ilość klatek na sekundę(ftp/s) background(22); // Tło smooth(4); // Ustawienie poziomu anty-aliasingu sphereDetail(120); // Szczegółowość rysowania sfer EARTH=createShape(SPHERE,EARTH_RADIUS); SUN=createShape(SPHERE, SUN_RADIUS); EARTH.setStroke(false); // Brak wyświetlania obrysu EARTH.setFill(#0D8FBC); // Kolor wypełnienia #0D8FBC (błękitny) SUN.setStroke(false); // Brak wyświetlania obrysu SUN.setFill(#F5E20A); // Kolor wypełnienia #F5E20A (żółty) } void draw() { background(22); // Wymazanie obrazu klatki translate(width/2,height/2,0); // Przesunięcie środka układu współrzędnych do centrum ekranu/okna programu shape(EARTH); // Rysunek Ziemi xSol=SUN_ORBIT*cos(tSol); ySol=SUN_ORBIT*sin(tSol); tSol+=TWO_PI/360; if (tSol>TWO_PI) tSol=0; pushMatrix(); translate(xSol,ySol,zSol); shape(SUN); popMatrix(); } W kolejnej, trzeciej części artykułu zajmiemy się światłem i ruchem obserwatora układu (do tej pory układ obserwowany jest z jednego miejsca), a Słońce i Ziemia uzyskają inny, atrakcyjniejszy wygląd. Do tego czasu warto poćwiczyć wykorzystanie funkcji pushMatrix() i popMatrix(). Przy pewnej wprawie stają się one nadzwyczaj użytecznym narzędziem.
  5. Wstęp W artykule przedstawione zostały wybrane, podstawowe metody operowania obiektami trójwymiarowymi w środowisku Processing 4.0 alpha 3 (rel. 17.01.2021). Pomimo, że zapoznanie się z tematem nie wymaga wcześniejszego doświadczenia z Processing, wskazana jest znajomość fundamentów programowania oraz pewien stopień swobody w budowaniu skryptów. Niekoniecznie w tym środowisku. Prawie wszystkie poruszane tematy uzupełnione są praktycznymi przykładami, składającymi się na kompletny, działający i jednocześnie nieduży program, który następnie może podlegać dalszym modyfikacjom. Co budujemy? W roku 1543 Mikołaj Kopernik opublikował w Norymberdze dzieło, które na zawsze zmieniło postrzeganie Wszechświata. Dzisiaj nie wyobrażamy sobie Układu Słonecznego inaczej niż grupy planet okrążających Słońce. Jednak ujęcie geocentryczne obecne jest w języku potocznym, sztuce, a nawet astronawigacji. Nasz program będzie próbą prezentacji układu Ziemia-Słońce tak, jakby to Ziemia znajdowała się w jego centrum, a nie odwrotnie. Zadaniem programu będzie modelowanie wzajemnego położenia nieruchomej Ziemi (EARTH) i krążącego wokół niej Słońca (SUN). Na koniec dla uatrakcyjnienia całości wprowadzony zostanie również trzeci obiekt: Sfera Niebieska (CELESTIAL_SPHERE). Na potrzeby budowy modelu przyjęto, że zarówno Ziemia jak i Słońce są idealnymi kulami. Założono również, że centrum trójwymiarowego układu współrzędnych, względem którego dokonywane będą przekształcenia, znajduje się w geometrycznym środku Ziemi. Dokonano także uproszczeń mechaniki ruchu Słońca. PShape i translate() Ponieważ program będzie wykorzystywał obiekty i przekształcenia dostępne jedynie w trzech wymiarach naszym pierwszym krokiem będzie ustawienie jednego z 5 dostępnych trybów renderingu na „P3D”. Należy tego dokonać podając trzeci parametr funkcji size() odpowiedzialnej za określenie rozmiarów okna, w którym wykonywany będzie program: size(1800,1000,P3D); // Rozmiar okna 1800/1000 pikseli. Rendering trójwymiarowy Program będzie dokonywał animacji ruchu Słońca wokół Ziemi, czyli sekwencyjnego wyświetlania klatek (frames) zawierających kolejne przekształcenia wzajemnej pozycji tych obiektów. Dlatego zawsze warto mieć na uwadze szybkość z jaką klatki są odtwarzane. Parametrem tym można sterować za pomocą funkcji frameRate(), przy czym domyślną wartością jest 60 fps/s. Na potrzeby programu w zupełności wystarczy 30 fps/s: frameRate(30); // Ilość klatek na sekundę(ftp/s) Ustawienia podstawowych parametrów programu kończą się wyborem koloru tła w skali odcieni szarości: Background(22); // Tło Processing oferuje szereg metod pozwalających na rysowanie obiektów trójwymiarowych. Jednym z wygodniejszych sposobów jest wykorzystanie typu danych PShape pozwalającego na przechowywanie kształtów, w tym kształtów trójwymiarowych. Użycie obiektu przechowującego kształt wymaga jego zadeklarowania: PShape EARTH; // Deklaracja obiektu przechowującego informacje o kształcie Ziemi Od momentu zadeklarowania EARTH jako obiektu typu PShape możliwe będzie wykorzystanie w stosunku do niego wszystkich metod przypisanych do typu PShape. Należy pamiętać, że przed użyciem obiektu musi być on przypisany do kształtu, który z kolei może być wczytany za pomocą funkcji loadShape() lub stworzony funkcją createShape(). Processing oferuje wiele sposobów zastosowania funkcji createShape() oraz zestaw predefiniowanych kształtów wybieranych za pośrednictwem parametrów funkcji. Możliwe jest również grupowanie wielu kształtów oraz wykorzystanie funkcji bez parametrów i zdefiniowanie kształtu później, w trakcie działania programu. W przypadku podania jako parametru kształtu predefiniowanego możliwe jest wykorzystanie w stosunku do niego parametrów kształtu podstawowego. Na przykład w stosunku do predefiniowanego kształtu SPHERE możliwe jest zastosowanie parametrów funkcji sphere(). Aby użyć obiektu EARTH w naszym programie przypisywany jest do niego nowy kształt będący sferą (predefiniowany kształt SPHERE - co zasadniczo odpowiada kształtowi Ziemi) z jednym parametrem określającym jej promień (EARTH_RADIUS). EARTH=createShape(SPHERE,EARTH_RADIUS); // Przypisanie do obiektu EARTH typu PShape predefiniowanego kształtu sfery o zadanym promieniu (jeden z dozwolonych parametrów funkcji podstawowej sphere()) Oczywiście parametr EARTH_RADIUS wymaga wcześniejszego zadeklarowania: int EARTH_RADIUS=150; // Promień Ziemi Samo zadeklarowanie obiektu EARTH i przypisanie do niego zadanego kształtu nie jest wystarczające do jego wyświetlenia. W tym celu konieczne jest użycie dodatkowo funkcji shape(). W programie zastosowano najprostsze jej wywołanie: shape(EARTH); // Rysunek Ziemi Podsumowując, prawidłowe wyświetlenie obiektu typu PShape wymaga: Deklaracji obiektu: PShape Przypisania do obiektu zadanego kształtu: (createShape() lub loadShape()) Wyświetlenia obiektu (wywołania): shape() Sfera w środowisku Processing jest obiektem szczególnym. Ponieważ jej wyświetlenie wymaga narysowania zbioru płaszczyzn, często konieczne jest podjęcie decyzji co do ich liczby, zapewniającej oczekiwane przybliżenie kształtu sfery. Określenie liczby wierzchołków siatki wykorzystywanej do rysowania sfery realizowane jest przez funkcję sphereDetail(). Rozdzielczość domyślna to 30, co pozwala na narysowanie stosunkowo precyzyjnej figury. Przy podejmowaniu decyzji o szczegółowości odtworzenia kształtu należy mieć jednak na uwadze ile sfer będzie rysowanych w tym samym czasie. Im większa liczba użytych płaszczyzn tym mniejsza liczba sfer będzie wyświetlana bez opóźnień wpływających na płynność działania programu. Ponieważ w naszym programie wykorzystywane są tylko 3 sfery możliwe jest ustawienie wysokiego poziomu szczegółowości: sphereDetail(120); // Określenie szczegółowości rysowania sfer Warto pamiętać, aby wywołanie funkcji sphereDetail() odbywało się przed wywołaniem createShape() oraz shape(). Na podstawie zdobytej wiedzy można pokusić się na zbudowanie pierwszej wersji programu, którego zadaniem będzie wyświetlenie sfery o promieniu 150 i szczegółowości 120. Miejsca w których umieszczone są poszczególne komendy nie są przypadkowe! int EARTH_RADIUS=150; // Promień Ziemi PShape EARTH; // Deklaracja kształtu przechowującego informacje o kształcie Ziemii void setup() { // Część programu wykonywana raz size(1800,1000,P3D); // Rozmiar okna 1800/1000 pixeli. Rendering trójwymiarowy frameRate(30); // Ilość klatek na sekundę(ftp/s) background(22); // Tło sphereDetail(120); // Szczegółowość rysowania sfer EARTH=createShape(SPHERE,EARTH_RADIUS); } void draw() { // Część program wykonywana w pętli shape(EARTH); // Rysunek Ziemi } Otrzymujemy widok sfery zniekształconej przez perspektywę, której centrum zlokalizowane jest w lewym górnym rogu ekranu. Ponadto, na jej powierzchni widoczne są linie płaszczyzn, z których została zbudowana, a jej kolor daleki jest od koloru Ziemi. Sam rysunek jest też trochę chropowaty. Zacznijmy od naprawienia mankamentów jakimi są kolor i linie na powierzchni obiektu. Jak już wcześniej wspomniano do rysunku Ziemi możliwe będzie wykorzystanie metod przypisanych do typu PShape. Za określenie sposobu wyświetlania koloru obrysu obiektu typu PShape odpowiada metoda setStroke(), natomiast za kolor i wypełnienie kształtu metoda setFill(). W naszym programie określamy je za pośrednictwem dwóch komend: EARTH.setStroke(false); // Ustawienie braku wyświetlania obrysu EARTH.setFill(#0D8FBC); // Kolor wypełnienia #0D8FBC (błękitny) Processing udostępnia wiele sposobów reprezentacji kolorów, które nie zostaną tutaj omówione. Zarówno do prostych jak i zaawansowanych projektów wystarczające jest używanie reprezentacji heksadecymalnej. Wybór żądanego koloru umożliwia wbudowane proste narzędzie „Color Selector” dostępne w menu „Tools”. Dla bardziej wymagających przedsięwzięć wygodniejsze jest jednak korzystanie z zewnętrznych narzędzi wyboru koloru. Za „chropowatość” linii odpowiada zjawisko aliasingu. Standardowo w trybie renderingu P3D anty-aliasing ustawiony jest na poziom 2x. W zależności od wydajności sprzętu możliwe jest zastosowanie wygładzania z poziomem 4x lub 8x. W programie zastosowano poziom 4, czyli czterokrotny poziom nadmiaru próbkowania bufora grafiki. smooth(4); // Ustawienie poziomu anty-aliasingu Ponieważ Ziemia znajduje się w centrum uwagi naszego programu konieczne jest na koniec nadanie należnego jej miejsca. W tym celu można, choć nie jest to jedyny sposób, wykorzystać niezwykle potężną funkcję translate(). W środowisku 3D translację można wyobrazić sobie jako przesunięcie środka układu współrzędnych x,y,z o zadaną wartość wzdłuż poszczególnych osi. Z funkcji należy korzystać bardzo ostrożnie. Przekształcenia geometryczne są wrażliwe na kolejność w jakiej wywoływane będą poszczególne funkcje. W konsekwencji narysowanie obiektu np. w środku układu współrzędnych, a następnie zastosowanie funkcji translate() nie spowoduje jego przesunięcia. Pozostanie on tam gdzie został pierwotnie narysowany, przesunie się natomiast sam układ współrzędnych. Przy okazji należy pamiętać, że w środowisku trójwymiarowym Processing, układ współrzędnych jest lewoskrętnym układem kartezjańskim, którego środek umiejscowiony jest w lewym górnym rogu ekranu. Wartości Y rosną w kierunku dołu ekranu, wartości X w kierunku prawej strony ekranu, natomiast wartości Z zwiększają się w kierunku ruchu do powierzchni ekranu i zmniejszają w kierunku w „głąb” ekranu. Do tej pory nie wskazano gdzie dokładnie ma zostać wyświetlona Ziemia. W konsekwencji została ona umieszczona przez program w środku układu współrzędnych, który znajduje się jak wspomniano w lewym górnym rogu ekranu. Aby przesunąć ją w docelowe miejsce konieczne jest przesunięcie środka układu współrzędnych do centrum okna programu. translate(width/2,height/2,0); // Przesunięcie środka układu współrzędnych do centrum ekranu/okna programu W tym konkretnym przypadku zamiast bezpośredniego podania przesunięcia wzdłuż osi X oraz osi Y wykorzystano zmienne systemowe height oraz width przechowujące rozmiary okna programu zdefiniowane na poziomie funkcji size(). W konsekwencji środek układu współrzędnych przesunął się o połowę szerokości okna wzdłuż osi X oraz połowę wysokości okna wzdłuż osi Y, nie zmieniając swojego położenia na osi Z. Po dokonaniu powyższych modyfikacji program przyjął następującą formę: int EARTH_RADIUS=150; // Promień Ziemi PShape EARTH; // Deklaracja kształtu przechowującego informacje o kształcie Ziemii void setup() { size(1800,1000,P3D); // Rozmiar okna 1800/1000 pixeli. Rendering trójwymiarowy frameRate(30); // Ilość klatek na sekundę(ftp/s) background(22); // Tło sphereDetail(120); // Szczegółowość rysowania sfer EARTH=createShape(SPHERE,EARTH_RADIUS); smooth(4); // Ustawienie poziomu anty-aliasingu EARTH.setStroke(false); // Brak wyświetlania obrysu EARTH.setFill(#0D8FBC); // Kolor wypełnienia #0D8FBC (błękitny) } void draw() { translate(width/2,height/2,0); // Przesunięcie środka układu współrzędnych do centrum ekranu/okna programu – kolejność jest ważna! shape(EARTH); // Rysunek Ziemi } W centrum okna wyświetlana jest gładka, niebieska sfera: W tym miejscu kończy się Część Pierwsza. Przed sięgnięciem do Części Drugiej, gdzie do programu dodamy funkcje odpowiedzialne za Słońce oraz jego obrót wokół Ziemi, a obrazy sfer zostaną nieco bardziej urealnione, zachęcam do eksperymentów z różnymi obiektami typu PShape i funkcją translate().
  6. @opp34 Dziękuję za miłe słowa. Dla mnie największą wartością tego projektu był walor poznawczy. Ilość wiedzy przyswojonej przy jego realizacji mnie samego wprowadziła w zdumienie. Projekt jest również zaprzeczeniem powtarzanej początkującym rady, z którą się często spotykałem, aby nie brać się za "budowanie katedry". Uważam, że właśnie warto to robić. Mam przeczucie, że w wielu przypadkach przygoda z programowaniem zaczyna się od sterowania lampką, a kończy na... sterowaniu lampką. Po szkole jaką dał mi patefon moja perspektywa patrzenia na projekty informatyczne totalnie się zmieniła. A co najważniejsze dalej mi się chce, bo po drodze odkryłem tyle interesujących ścieżek... Myślę, że teraz jeszcze przez chwilę poeksperymentuję z czujnikiem laserowym. Może odpowie na niektóre moje dylematy. Popracuję też z plikami dźwiękowymi pod kątem ich analizy. Wciągnęło mnie to. Chciałbym też zrobić ładny projekt w Processingu. Mam pomysł na interesującą strukturę, której jak mi się wydaje po przejrzeniu projektów w OpenProcessing, jeszcze nikt nie prezentował. Podszkolę się w operowaniu klasami.
  7. Pozdrawiam wszystkich forbotowych Forumowiczów w ostatni dzień Świąt! Życzę Wam wszystkiego najlepszego, wyłącznie udanych projektów i niegasnącej radości z rozwijania Waszych pasji! Dla mnie jest to dzień świąteczny podwójnie, bo wreszcie udało mi się skończyć mój pierwszy poważny projekt jakim była rejestracja i odtworzenie dźwięku emitowanego przez standardowy przetwornik elektroakustyczny przy wykorzystaniu czujnika odległości HC_SR04. Zadanie, które wydawało mi się z pozoru łatwe, z biegiem dni i tygodni przerodziło się w długi i skomplikowany proces gromadzenia wiedzy, jej przyswajania, a następnie udanych i nieudanych prób zastosowania w praktyce. Projekt, który miał trwać najwyżej tydzień, pochłonął mnie na ponad 3 miesiące, wywołując prawie obsesję i maksymalne podenerwowanie rodziny. Nakładała się na to także końska dawka sceptycyzmu i ironii serwowana mi na Forum przez doświadczonych Forumowiczów. W tym miejscu chciałem jeszcze raz podziękować Panu Markowi (marek1707), który chyba jako jedyny dostrzegał światełko w tunelu. Uzyskałem od niego duże wsparcie i trochę cierpkich uwag. Założenia projektu były dosyć proste. HC_SR04 jest ultradźwiękowym czujnikiem odległości, który skierowany na membranę głośnika akustycznego stosunkowo precyzyjnie dokonuje pomiarów jej wychylenia w czasie emisji dźwięku. Wychylenia te można zarejestrować, a następnie podjąć próbę odtworzenia zapisanego w ten sposób sygnału. Jak łatwo zauważyć zaangażowane są tu dwa zasadnicze procesy: cyfrowa rejestracja fizycznych drgań membrany, które mają charakter analogowy, oraz proces odwrotny, czyli przetworzenie cyfrowego zapisu na drgania membrany generujące falę dźwiękową rozchodzącą się w powietrzu. Razem: konwersja AD/DA. Swoją drogą sama fala akustyczna powstająca w wyniku drgań ma dla tego projektu znaczenie wtórne. Pomiarów i emisji drgań można byłoby dokonywać w próżni. Sam dźwięk jest efektem ubocznym. Reasumując, moim zadaniem było rozpoznanie i zapisanie drgań fizycznego nośnika, a następnie ich emisja w formie jak najbardziej zbliżonej do oryginalnej. W roli przetwornika AD wykorzystałem płytkę mikrokontrolera Arduino Uno z podłączonym do niej czujnikiem odległości. Sygnał generowany przez czujnik skierowałem bezpośrednio do portu szeregowego czytanego przez komputer PC pełniący rolę przetwornika DA. Ze względu na specyfikę środowiska IDE Arduino wymagało to zaangażowania drugiego systemu, na który wybrałem IDE Processing w wersji 4.0, który jak mi się wydawało miał pozwolić na wygodne przetwarzanie i zapisywanie otrzymywanych danych oraz elastyczną konstrukcję interfejsu użytkownika. Przy okazji wspomnę, że z powodów, których nie potrafię jednoznacznie wyjaśnić, napotkałem po drodze na dziwne problemy z optymalnym ustawieniem szybkości transmisji danych. Ostatecznie, właściwym parametrem okazało się dopiero 230400 baudów. Widok instalacji czytającej sygnał i wysyłającej do PC (talia kart ma idealną grubość dla ustawienia czujnika): W trakcie realizacji procesu rejestracji drgań na pierwszy plan wysunęło się zagadnienie częstotliwości oraz precyzji próbkowania. HC-SR04 jest urządzeniem dokładnym, ale nie dokładnym na tyle, by rejestrować drgania membrany ustawionego na maksymalną głośność głośnika średnio tonowego o średnicy ok. 10 cm przy częstotliwościach wyższych niż 160-170 Hz. Jakość próbkowania, a tym samym działanie całego systemu, były więc pierwotnie zdeterminowane przez charakterystykę źródła drgań. Im wyższa była ich amplituda tym precyzyjniej czujnik dokonywał ich odczytu. Był to bolesny mankament, z góry ograniczający zakres rejestrowanych częstotliwości. Na szczęście nie zmieniało to poprawności samej idei projektu. Inny aspekt próbkowania jakim jest częstotliwość i regularność pomiarów rozwiązałem poprzez bezpośrednie operowanie na rejestrach mikrokontrolera w reżimie cyklicznych przerwań osiągając poziom 2000 próbek na sekundę, powyżej których czujnik odmawiał współpracy. W świetle jednak opisanych powyżej ograniczeń, związanych z amplitudą wychyleń membrany, ten aspekt przestał być krytyczny. Ostatecznie czujnik został ustawiony na częstotliwość 500 pomiarów na sekundę, co zapewniło relatywną poprawność odczytów przy istniejących ograniczeniach emitera oraz samego czujnika. Ostatnią istotną rzeczą przy realizacji samych pomiarów była optymalizacja odległości czujnika od membrany. W wyniku doświadczeń okazało się, że wynosi ona ok. 2 cm. Posiadanie zestawu odczytów czujnika umożliwiało dokonanie ich analizy w celu przetworzenia ponownie na formę analogową. To pozornie proste zadanie stało się jednak, w miarę zagłębiania się w nie, nadzwyczaj trudne. Niewinnie wyglądający ciąg liczb reprezentujących czas rejestrowany przez czujnik, był obrazem złożonego ruchu falowego. Na obraz tego ruchu składało się jedna lub więcej fal o określonej amplitudzie, częstotliwości oraz fazie. Do analizy składowych tak zapisanego drgania wykorzystuje się niebanalną matematycznie transformację Fouriera. Z chwilą wyraźnego uświadomienia sobie tego faktu stanąłem na projektowym rozdrożu. Albo czekało mnie przekształcanie fourierowskich wzorów na program, albo rzucenie wszystkiego i wyjechanie w Bieszczady, albo takie przedefiniowanie projektu, aby mieszcząc się w jego założeniach, nie robić z patefonu skomplikowanego analizatora drgań. Surowy sygnał odebrany z Arduino, oraz jego zbliżenie: Wybrałem ścieżkę przez chaszcze. Stworzyłem własną metodę analizy, która nie wchodząc w jej zawiłości, polega na przepuszczaniu zapisu drgań przez kolejne sita zmieniających się oczek amplitudy. Mam wrażenie, że Fourier zrobił podobnie, ale zgrabniej to zapisał. Zaletą mojej metody było to, że na każdym kroku programowania miałem poczucie zrozumienia tego, co robię. Z Fourierem nie do końca mi to wychodziło. Aby jeszcze bardziej ułatwić sobie zadanie postanowiłem ponadto (chwilowo) wyzbyć się problemu amplitudy oraz fazy i skoncentrować wyłącznie na częstotliwości. W tym celu dokonałem ograniczenia źródła drgań do pojedynczej fali sinusoidalnej, co jako bonus pozwoliło mi przetestować mój autorski sposób analizy w praktyce. Dzień, w którym zobaczyłem na ekranie komputera dokładną wartość częstotliwości mierzonych drgań był jednym z najprzyjemniejszych w moim życiu. Przede mną nadal stoi jeszcze zadanie zbadania bardziej złożonych fal, ale to już chyba nie z tym czujnikiem i nie szybko. Główne etapy przetwarzania sygnału: 1) Przesunięcie wartości 2) Normalizacja (konwersja amplitudy do poziomu 1) 3) Redukcja (jednorazowe przepuszczenie znormalizowanego sygnału przez „sito” amplitudy /nazywam go poziomem ciszy/. Badanie sygnału złożonego wymaga wielokrotnych iteracji tego procesu): 4) Obliczenie częstotliwości na podstawie zredukowanego sygnału: Jak wspomniałem przetwarzanie sygnału do postaci pozwalającej na jego odtworzenie odbywało się w środowisku IDE Processing ver. 4. w systemie IOS. Z perspektywy dotychczasowych doświadczeń nie polecam tego rozwiązania. Processing działający w tym systemie potrafi wygenerować różne problemy. Na przykład, przy próbie eksportu programu do pliku wykonywalnego Processing dokonał niesygnalizowanej i trudnej do szybkiej identyfikacji zmiany numerów portów (pomijając fakt, że eksport się nie udał, a plik wykonywalny nie działał poprawnie). Tego rodzaju niespodzianki naprawdę potrafią zepsuć humor, co najmniej na czas poszukiwania usterek. Przyzwyczaić się też trzeba do koncepcji odświeżanych ze standardową częstotliwością ramek. Cechę tę można wykorzystać z pożytkiem dla algorytmu, ale zapomnienie o niej może prowadzić do całej listy trudnych do identyfikacji anomalii, zwłaszcza w programach o rosnącej objętości i złożonej strukturze. Tak było w moim przypadku. Ramki kosztowały mnie godziny poprawiania kodu. Ogólnie rzecz biorąc Processing jako doskonałe narzędzie programowania procesów i struktur słabo radzi sobie z wykorzystaniem go do budowy prostych interfejsów. Kod, zwłaszcza pisany niedbale szybko staje się zagmatwany i nieelastyczny. Operowanie setkami współrzędnych relatywnie prostych obiektów, ich metodami i przekształceniami po pewnym czasie może stać się prawdziwą udręką. Rozwiązaniem może tu być stworzenie całego systemu klas i późniejsze operowanie tylko na nich. Do budowy prostego patefonu nie wydawało mi się to niezbędne. Błędne założenie. Processing w moim projekcie przetwarzał surowy sygnał z czujnika do ciągu częstotliwości wyrażonych w hercach gotowych do odtworzenia na dowolnym głośniku przy wykorzystaniu każdego popularnego obiektu służącego do generowania oscylatora, jak np. SinOsc w Processing, czy funkcji tone() w Arduino. Początkowo zamierzałem skierować ciąg częstotliwości z powrotem do Arduino i odtworzyć na dołączonym do niego głośniku zewnętrznym (pod ten cel powstało w programie wiele nie wykorzystanych struktur), ale perspektywa utknięcia na problemach doboru właściwego rezystora, głośnika oraz wizja plączących się kabli skłoniła mnie do skierowania sygnału po prostu na wewnętrzny głośnik komputera. Widok interfejsu: Co robi dzisiaj system, którego działanie za chwilę będziecie mogli obejrzeć? Rejestruje drgania membrany głośnika średnio tonowego o średnicy 10 cm w zakresie częstotliwości 30-160Hz, przetwarza go, wizualizuje a następnie odtwarza na głośnikach komputera. To jednocześnie wiele i niewiele. W trakcie przetwarzania dźwięku występują co najmniej dwa dziwne zjawiska, których nie potrafię ani dobrze opisać, ani tym bardziej wytłumaczyć ich istoty. Te anomalie są dla mnie zagadką. Wrócę do nich na koniec. Po tym przydługim wstępie, w końcu finał: Na początek zapis i odtworzenie przez system pojedynczej fali sinusoidalnej o stałej częstotliwości 50Hz.Być może jest to pierwszy zapis dźwięku czujnikiem HC-SR04 w historii cywilizacji. Trudno mi to ocenić. Cytując znanego autora zapytanego, co ostatnio czytał, mógłbym odpowiedzieć: „Nie wiem, nie czytam książek. Ja je piszę”. Krótki komentarz. Fala tworzona jest w mobilnej aplikacji „Frequency Generator” na Androida, komunikującej się za pośrednictwem bluetooth z głośnikiem. Widać jak silne wychylenia membrany powoduje częstotliwość 50Hz! Następnie rozpoczynany i kończony jest zapis dźwięku za pomocą przycisków na płytce Arduino. Przechodzę do interfejsu i wybieram odtwarzanie spowolnione – służyło mi ono głównie do analizy sygnału. Taktowane jest szybkością odświeżania ramek – w tym przypadku 200 ftp. Na koniec wybieram odtwarzanie dźwięku z prędkością rzeczywistą. Wyświetlanie interfejsu ulega wtedy zawieszeniu. Ramki się nie odświeżają. Odtwarzania z prędkością rzeczywistą nie da się zatrzymać. Trzeba byłoby je oprogramować w przerwaniach. Jak widać patefon poprawnie zapisał i odtworzył sygnał o częstotliwości 50Hz. Występujące zakłócenia nie mają istotnego znaczenia. Ich charakter jest przypadkowy. Wynikają z aktualnych warunków w jakich dokonywany jest zapis: ruchy powietrza wokół głośnika, drgania stołu, głośność odtwarzania, dźwięki tła itp. Trzask występujący na końcu zapisu jest wywołany przez błąd algorytmu. Zidentyfikowałem go, ale już nie chciało mi się go naprawiać, bo przyzwyczaiłem się do niego jako sygnału końca nagrania. Poniżej, wideo samego interfejsu i możliwości jakie daje w zakresie analizy nagrania. Widać na nim, że zakłócenia jakie wystąpiły powodują kilkukrotnie zmianę częstotliwości z 50 na 46 Hz. Spowolniony dźwięk odtwarzany jest w pętli. Przy pewnej staranności możliwe jest uzyskanie zupełnie „czystego” nagrania, praktycznie bez zakłóceń. Jednak celowo zamieszczam to, bo oddaje dobrze klimat pracy nad patefonem: Teraz crème-de-la-crème projektu, czyli nagranie pojedynczej fali sinusoidalnej o zmiennej częstotliwości w zakresie 30-170Hz i z powrotem (tony ok. B0-F3). To pozwala już zagrać prostą melodię, postanowiłem jednak oszczędzić sobie i Wam moich popisów instrumentalnych: Jak już wcześniej wspominałem w nagraniach występują dwie anomalie. Na poniższym nagraniu widać je doskonale: 1) Deszcz spadających cegieł. Są to rozległe zakłócenia występujące przy każdej próbie. Pomimo, że mają one charakter przypadkowy i występują w różnym nasileniu, ich ogólny wygląd jest bardzo podobny. Obejmują ograniczone zakresy częstotliwości, najczęściej symetrycznie. Podejrzewam, że ich źródłem są niesterylne warunki nagrania. Nie mam jednak pewności. To by było za proste… 2) Skokowe zmiany częstotliwości. Jest to dużo poważniejszy problem. Im wyższa zarejestrowana częstotliwość, tym krok jej zmiany ma większą wartość. Przy sygnale wzrastającym i opadającym powstaje zawsze charakterystyczny „dach pagody”. Myślę, że ten efekt jest wierzchołkiem głębszego problemu. Może transformacja Addera obarczona jest jakąś pierwotną wadą? (tutaj obstawiałbym 80%, że tak) Ach! Jakże chciałbym porozmawiać z Fourierem! Może jest to jakieś zjawisko fizyczne, które mi umyka? Może to banalne, laickie błędy programistyczne, których nie potrafię zidentyfikować? Im głębiej w las, tym więcej drzew. Ciemność. Ciemność, widzę….
  8. Dobry wieczór! Dawno mnie tu nie było, ale zapewniam, że zaglądam codziennie. Pracowałem nad "kataryną". Działa już naprawdę fajnie. Mam jednak duży problem z rejestracją wychyleń membrany. W chwili obecnej jestem w stanie zarejestrować dźwięk w zakresie mniej więcej 2 oktaw - trzeciej i czwartej, przy częstotliwości próbkowania 500Hz - nie ma sensu wchodzić wyżej, bo i tak ruch membrany przy wyższych dźwiękach jest zbyt słaby na dobrą rejestrację. Pytanie do eksperta. Jak domowym sposobem zwiększyć amplitudę wychyleń membrany głośnika bez zwiększania głośności i powierzchni membrany? Może ktoś robił taki projekt? (osiągnąłem dostępny mi max.)
  9. Dzień dobry! Budowałem patefon, a skonstruowałem oscyloskop! Teraz mogę mierzyć każdy pulsujący sygnał z Arduino. Jestem bardzo zadowolony
  10. To jest bardzo ciekawe nagranie. Sygnał 60Hz. Widać jak na początku czujnik się "męczy" z rozpoznaniem częstotliwości. Potem zaczyna bardzo ładnie czytać. Na koniec sygnał źródłowy zostaje wyciszony. Co interesujące własna fala czujnika przestała wtedy być widoczna. Uzyskany zapis ciszy stał się wolny od zakłóceń.
  11. Zapis 120Hz. Widać wyraźne odkształcenia. Przyczyn zakłóceń może być wiele. Powtórzenie warunków idealnych wymaga średnio 5-10 prób. Czasami, w ogóle HC-SR04 się buntuje i nagranie trzeba powtórzyć po całkowitym resecie, albo następnego dnia...
  12. Dzień dobry! Dzień Chwały nadchodzi już wielkimi krokami! Przesyłam video pokazujące jak HC-SR04 rejestruje ciszę (niestety o tej porze nie mogę buczeć głośnikiem). Jak się należało spodziewać czujnik sam z siebie generuje identyfikowalną falę. Jednak to nic nie szkodzi. Rejestrowany sygnał się ponad nią wybija - sprawdzone. Liczba tych widocznych prążków w czasie jest wprost proporcjonalna do zarejestrowanej częstotliwości sygnału. Żeby rozpoznać częstotliwość wystarczy je zliczać w określonych odstępach czasu. Im gęściej są upakowane, tym częstotliwość sygnału większa. Przepraszam, że Kataryna jeszcze w takiej rozsypce. Na premierę nagrania jeszcze ją podrasuję.
  13. Dobry Wieczór, Wiem już, co robią te moje "filtry". Redukują głębię bitową. Redukują ją do 1 bitu! To pozwala na bardzo sprawne obliczanie częstotliwości. To też usprawnia robienie przekształceń dla prostych fal, które można teraz robić w pamięci, albo kierować się intuicją. W ten sposób doszedłem do zasady obliczania składowych fali przedstawionej powyżej - czyli parametrów dwóch używanych oscylatorów dla 1 bitowych próbek. Z częstotliwościami na razie jest spokój. Niestety, pojawił się kolejny problem. Wylazły skądś amplitudy i fazy. To jest z pozoru proste, ale... przy składaniu sampli pojawia się trzask wynikający z ucięcia oscylatorów w miejscu zakończenia okresu fali źródłowej i wznowienia ich kolejnego cyklu. Albo coś źle robię, albo nadal nie rozumiem. Pewnie i to, i to. Niemniej jednak, jednak już dzisiaj mógłbym Wam zaprezentować Patefon bezbłędnie rozpoznający proste fale sinusoidalne o leżącej w granicach próbkowania częstotliwości. Może się sprężę i oderwę od tych trzasków jutro. Nie dają mi spokoju, bo psują mi koncepcję zwiększenia głębi bitowej.
×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.