Animacija fluida u okruženju s preprekama

FER >> ZEMRIS >> Racunalna grafika
SIMPLE
Vizualizacija

Klasa Visualizer sadrži metode potrebne za vizualizaciju rezultata dobivenih radom objekta klase Solver. Zbog toga što koristi rezultate iz toga objekta ova klasa mu mora imati pristup i u svom konstruktoru očekuje predavanje podatka tipa Solver.

Klasa Visualizer podržava četiri različita načina iscrtavanja rezultata. To su prikaz polja tlaka, prikaz polja brzina, prikaz vektora brzina i na kraju strujnica fluida. Svaki od četiri načina je ostvaren u jednoj od četiri javne metode. Da bi željena veličina bila iscrtana, u glavnom se programu nakon instanciranja objekata klasa Solver i  Visualizer  i nakon pozivanja metoda objekta tipa Solver da bi se dobili neki rezultati, treba odrediti jedna od metoda za iscrtavanje iz ove klase.

Iscrtavanje tlaka

Prva u nizu metoda koje omogućuju vizualizaciju rezultata simulacije fluida je drawPressure(). Njen je pseudokod:


    drawPressure()
        izračunaj srednju vrijednost tlaka
        za svaki element mreže
            izračunaj intenzitet tlaka u odnosu na srednju vrijednost
            skaliran na 0-255
            postavi odgovarajući element colorMap-a na izračunati intenzitet
        iscrtaj colorMap
    kraj metode


Srednja vrijednost tlaka se računa da bi se dobila procjena raspona vrijednosti tlaka na cijeloj mreži. Naime, ako su sve vrijednosti jako male, dobivena slika će biti pretamna. Ako su vrijednosti visoke, slika će biti presvijetla. Jednostavno traženje maksimuma i minimuma tlaka također nije dovoljno. U tom slučaju se može dogoditi da praktički svi iznosi na mreži, osim na par elemenata, budu jako niski. To se može dogoditi pri toku oko oštre prepreke. Tad bi dobivena slika bila previše kontrastna. Većina promjena bi bila izgubljena u crnilu, dok bi samo nekoliko elemenata bilo osvijetljeno. Slično bi se dogodilo da je situacija obratna i da su  iznosi tlaka vrlo visoki u većini elemenata, a relativno niski u njih par. Tada bi većina detalja bila izgubljena u presvijetlim nijansama.


Zbog takvih problema najjednostavniji pristup je koristiti srednju vrijednost. Neke će visoke vrijednosti postati previsoke i dostići maksimum osvijetljenosti gdje nema razlika u nijansama, dok će ekstremi na suprotnoj strani biti jako tamni, međutim najveća diferencijacija boja će biti upravo na onoj razini na kojoj se nalazi većina elemenata.


Izračunatim intenzitetima se puni polje colorMap, koje se na kraju iscrtava pozivom posebne metode.

 

Iscrtavanje brzina

Metoda drawVelocities() radi dosta slično prethodnoj metodi. No, pošto brzina ima dvije komponente, u x i u y smjeru, ova metoda omogućava i izbor prikaza jedne od njih. Argumenti su dvije logičke vrijednosti od kojih se jedna odnosi na brzinu u x smjeru, a druga na brzinu u y smjeru. Ako je argument logička jedinica, odgovarajuća brzina će se iscrtavati, a ako je logička nula, neće. Oba argumenta mogu biti true, što znači da će se iscrtavati obje komponente.


Pseudokod ove metode je gotovo identičan prethodnome:


    drawVelocities(bool, bool)
        ako treba iscrtavati brzinu u x smjeru
            izračunaj srednju brzinu u x smjeru
        ako treba iscrtavati brzinu u y smjeru
            izračunaj srednju brzinu u y smjeru
        izračunaj ukupnu srednju brzinu
        za svaki element mreže
            ako treba iscrtavati brzinu u x smjeru
                izračunaj intenzitet brzine u x smjeru skaliran na 0-255
                postavi odgovarajući element colorMap-a
            inače
                postavi odgovarajući element colorMap-a na nulu
            ako treba iscrtavati brzinu u y smjeru
                izračunaj intenzitet brzine u y smjeru skaliran na 0-255
                postavi odgovarajući element colorMap-a
            inače
                postavi odgovarajući element colorMap-a na nulu
        iscrtaj colorMap
    kraj metode


Računanje srednjih vrijednosti brzina u određenim smjerovima identično je računanju srednjih vrijednosti tlaka. Ukupna srednja vrijednost se dobije kao aritmetička sredina srednjih vrijednosti u x i y smjeru.

 

Iscrtavanje iz mape boja

Na kraju obje prethodne metode se poziva metoda drawFromColorMap() koja iscrtava podatke sadržane u colorMap-u. Kod ove metode je jednostavan:


    public void drawFromColorMap()
        for (int i = 0; i < problemWidth; i++)
        {
            for (int j = 0; j < problemHeight; j++)
            {
                b.Color = colorMap[i, j];
                gr.FillRectangle(b, (float)(offsetX + i * zoom),
                    (float)(offsetY + j * zoom), (float)(zoom),
                    (float)(zoom));
            }
        }
    }


Dakle za svaki se element mreže jednostavno iscrtava pravokutnik boje pohranjene u colorMap-u. Mjesto iscrtavanja je određeno indeksom elementa s obzirom da mjesto u memoriji računala odgovara mjestu u simuliranom sustavu. Točne koordinate se dobijaju množenjem indeksa iznosom zoom. Istom varijablom određena je i veličina pravokutnika, odnosno kvadrata. Točnije veličina odgovara umnošku dimenzija jednog elementa mreže i vrijednosti zoom, ali kako su dimenzije mreže jedinične, mjere su kao u gornjem kodu. Cijelo je područje dodatno pomaknuto za iznose offsetX i offsetY u x i y smjerovima da bi se iscrtavanje vršilo centrirano u danome prostoru.

 

Iscrtavanje vektora brzina

Dosadašnje metode su direktno iscrtavale vrijednosti temeljnih veličina tlaka i brzine. Takav prikaz nije uvijek dovoljno informativan, koristan i, u krajnjoj liniji, zanimljiv. Iako je gledajući raznobojne brzine moguće zamisliti kako se fluid giba, prirodniji je prikaz smjera vektora brzine jer se na taj način neposredno može vidjeti smjer gibanja fluida. Sljedeće dvije metode pokušavaju upravo to.


Prva od njih je drawStreamlines(). Ova metoda iscrtava vektore brzina u čvorovima mreže. Krajnja slika u ovome slučaju će dakle biti niz linija usmjerenih u ovisnosti o vektoru brzine. Duljina će ovisiti o normi vektora. Linije su pozicionirane u centrima elemenata mreže, tj. polovišta linija su točno na koordinatama čvorova. Kod ove metode:


    public void drawStreamlines ()
    {
        double zoomDivider = 20;
        double meanVelocity = (getMeanVelValue(0) + getMeanVelValue(1)) / 2;
        clearPicBox(System.Drawing.Color.Black);
        for (int i = 0; i < problemWidth; i++)
        {
            for (int j = 0; j < problemHeight; j++)
            {
                p.Color = System.Drawing.Color.Red;
                gr.DrawLine(p, (float)(offsetX + i * zoom - 0.5 *
                solv.vel[i, j, 0] / meanVelocity * zoom/zoomDivider),
                (float)(offsetY + j * zoom - 0.5 * solv.vel[i, j, 1] /
                meanVelocity * zoom/zoomDivider), (float)(offsetX + i * zoom),
                (float)(offsetY + j * zoom));
                p.Color = System.Drawing.Color.White;
                gr.DrawLine(p, (float)(offsetX + i * zoom), (float)(offsetY +
                j * zoom), (float)(offsetX + i * zoom + 0.5 *
                solv.vel[i, j, 0] / meanVelocity * zoom/zoomDivider),
                (float)(offsetY + j * zoom + 0.5 * solv.vel[i, j, 1] /
                meanVelocity * zoom/zoomDivider));
            }
        }
        obstacleVisualization();
    }

 

Iscrtavanje strujnica

Druga metoda koja iscrtava smjerove gibanja fluida trebala bi dati najbolje rezultate, ali zato je računalno najzahtjevnija i znatno usporava rad aplikacije. Problem s prethodnom metodom je da iscrtava linije koje predstavljaju brzine u čvorovima mreže. Međutim, iako diskretiziran, sustav se simulira da bi se dobile kontinuirane veličine. Skokoviti prikaz brzina, iako informativan, a ujedno računalno nezahtjevan, nije vizualno najatraktivniji. Ideja na kojoj počiva metoda drawStreams() je interpolacija. Stvara se novo polje double[,] marker. Ovo polje sadrži proizvoljan broj elemenata, čestica, koje su inicijalno postavljene jednoliko u sustavu. Svaka se čestica zatim pomiče u ovisnosti o brzini na mjestu na kojem se nalazi. Ako se ne nalazi točno na nekom od čvorova, što je s double preciznošću položaja gotovo sigurno, brzinu je potrebno dobiti interpolacijom četiri susjedna iznosa. Nakon pomaka u ovisnosti o tom interpoliranom iznosu na novom će položaju trebati ponovno računati brzinu. Ovo se ponavlja za svaki pomak i za svaku česticu uvedenu u sustav. Očito je da su računalni zahtjevi ogromni. Potrebe iscrtavanja u praksi za red veličine premašuju potrebe iteracijskog postupka algoritma SIMPLE.
Kod metode drawStreams() je najkompliciraniji od dosad prikazanih u ovoj klasi. U pseudokodu izgleda kako slijedi:

    drawStreams()
        za onoliko čestica koliko ih se traži
            postavi inicijalni položaj čestice
        za sve čestice
            izračunaj interpoliranu brzinu na položaju čestice
            ako čestica nije izašla iz područja simulacije
                iscrtaj česticu
            pomakni česticu u ovisnosti o novoizračunatoj brzini
    kraj metode


S obzirom da se simulira fluid koji je zatvoren u simuliranom sustavu, naizgled je provjeravanje izlaska čestice iz područja simulacije nepotrebno. Naime, ako fluid ne može izaći, a čestice efektivno putuju nošene fluidom, to ne bi trebalo biti moguće. Postoje ipak najmanje dva razloga zašto je takav događaj ipak moguć. Prvo, da bi se ubrzalo iscrtavanje i naglasile strujnice čestice se kreću brzinama koje su veće od brzine fluida, ali linearno ovisne o njoj. Tako čestica može izaći iz sustava tamo gdje bi fluid naglo skrenuo. Drugo, ako se radi o sustavu kroz koji fluid protječe, dakle ima utok i istok, tada i fluid napušta sustav, noseći čestice sa sobom. Pitanje što učiniti s takvim česticama se može obraditi na niz načina. U ovoj se implementaciji jednostavno zanemaruju.


Najzahtjevniji dio gornjeg pseudokoda je računanje interpoliranih brzina. Zbog bolje preglednosti ovaj je dio koda izdvojen u posebnu metodu. Njen kod je:


    private void getMarkerVel (double[,,] marker, int i, int j, double[] vel)
    {
        double[] A = new double[2];
        double[] B = new double[2];
        double a1, a2, b1, b2;
        int ix, iy;
        ix = (int)marker[i, j, 0];
        iy = (int)marker[i, j, 1];
        a1 = marker[i, j, 0] - (int)marker[i, j, 0];
        a2 = 1 - a1;
        b1 = marker[i, j, 1] - (int)marker[i, j, 1];
        b2 = 1 - b1;
        try
        {
            A[0] = a2 * solv.vel[ix, iy, 0] + a1 * solv.vel[ix + 1, iy, 0];
            A[1] = a2 * solv.vel[ix, iy, 1] + a1 * solv.vel[ix + 1, iy, 1];
            B[0] = a2 * solv.vel[ix, iy + 1, 0] + a1 *
                solv.vel[ix + 1, iy + 1, 0];
            B[1] = a2 * solv.vel[ix, iy + 1, 1] + a1 *
                solv.vel[ix + 1, iy + 1, 1];
        }
        catch
        {
            vel[0] = 0;
            vel[1] = 0;
            return;
        }
        vel[0] = b2 * A[0] + b1 * B[0];
        vel[1] = b2 * A[1] + b1 * B[1];
    }


Sam je kod jednostavan. Varijable a1, a2, b1 i b2 sadrže udaljenosti od lijevih i desnih, odnosno gornjih i donjih susjeda. Odnos udaljenosti govori koliki je utjecaj svakog od čvorova. Polja A i B su pomoćna i sadrže međurezultate. Krajnji rezultat se dakle dobija u dva stupnja, prvo interpoliranjem gornjeg i donjeg para susjeda po x osi, a zatim interpoliranjem ta dva rezultata po y osi. Iako jednostavan, postupak se izvršava za svaki pomak za svaku česticu, iz čega proizlazi računalna zahtjevnost.