Ohjelmointi

3D-graafinen Java: Tee fraktaalimaisemia

3D-tietokonegrafiikalla on monia käyttötarkoituksia - peleistä tietojen visualisointiin, virtuaalitodellisuuteen ja muuhun. Useimmiten nopeus on ensiarvoisen tärkeää, joten erikoistuneet ohjelmistot ja laitteistot ovat välttämättömiä työn saamiseksi. Erityiskäyttöiset grafiikkakirjastot tarjoavat korkean tason sovellusliittymän, mutta piilottavat, miten todellinen työ tehdään. Nenä-metalli-ohjelmoijina se ei kuitenkaan riitä meille! Aiomme sijoittaa API: n kaappiin ja tarkastella kulissien takana sitä, miten kuvat todella syntyvät - virtuaalimallin määrittelystä sen todelliseen renderointiin ruudulle.

Tarkastelemme melko spesifistä aihetta: muodostetaan ja renderoidaan maastokarttoja, kuten Marsin pinta tai muutama kullan atomi. Maastokartan renderointia voidaan käyttää muuhun kuin esteettiseen tarkoitukseen - monet datan visualisointitekniikat tuottavat dataa, joka voidaan renderoida maastokarttoina. Aikomukseni ovat tietysti täysin taiteelliset, kuten alla olevasta kuvasta näet! Jos haluat, tuotettava koodi on riittävän yleinen, että vain pienellä säätämisellä sitä voidaan käyttää myös muiden 3D-rakenteiden kuin maastojen renderointiin.

Napsauta tätä, jos haluat tarkastella ja käsitellä maastosovellusta.

Valmistautuessamme tämän päivän keskusteluun ehdotan, että luet kesäkuun "Piirrä kuvioituja palloja", jos et ole vielä tehnyt niin. Artikkeli osoittaa säteiden jäljittämisen lähestymistavan kuvien renderointiin (säteiden ampuminen virtuaalikohtaukseen kuvan tuottamiseksi). Tässä artikkelissa piirrämme kohtauselementit suoraan näytölle. Vaikka käytämme kahta erilaista tekniikkaa, ensimmäinen artikkeli sisältää jonkin verran taustamateriaalia java.awt.kuva paketti, jota en yritä uudelleen tässä keskustelussa.

Maastokartat

Aloitetaan määrittelemällä a

maastokartta

. Maastokartta on toiminto, joka kartoittaa 2D-koordinaatin

(x, y)

korkeuteen

a

ja väri

c

. Toisin sanoen maastokartta on yksinkertaisesti toiminto, joka kuvaa pienen alueen topografiaa.

Määritellään maastomme käyttöliittymänä:

julkinen käyttöliittymä Maasto {public double getAltitude (double i, double j); julkinen RGB getColor (kaksinkertainen i, kaksinkertainen j); } 

Tätä artikkelia varten oletamme sen 0,0 <= i, j, korkeus <= 1,0. Tämä ei ole vaatimus, mutta antaa meille hyvän idean mistä maastomme katsomme.

Maastomme väriä kuvataan yksinkertaisesti RGB-triplettinä. Mielenkiintoisempien kuvien tuottamiseksi voimme harkita muiden tietojen, kuten pinnan kiiltävyyden jne., Lisäämistä. Toistaiseksi seuraava luokka kuitenkin tekee:

julkisen luokan RGB {yksityinen kaksinkertainen r, g, b; julkinen RGB (kaksinkertainen r, kaksinkertainen g, kaksinkertainen b) {this.r = r; tämä.g = g; tämä.b = b; } julkinen RGB-lisäys (RGB rgb) {palauta uusi RGB (r + rgb.r, g + rgb.g, b + rgb.b); } julkinen RGB-vähennys (RGB rgb) {palauta uusi RGB (r - rgb.r, g - rgb.g, b - rgb.b); } julkinen RGB-asteikko (kaksinkertainen asteikko) {palauta uusi RGB (r * asteikko, g * asteikko, b * asteikko); } private int toInt (kaksinkertainen arvo) {return (arvo 1.0)? 255: (int) (arvo * 255,0); } public int toRGB () toInt (b); } 

RGB luokka määrittelee yksinkertaisen värisäiliön. Tarjoamme joitain perusominaisuuksia väriaritmetian suorittamiseksi ja liukulukuvärin muuntamiseksi pakatuksi kokonaisluvuksi.

Transsendenttinen maasto

Aloitamme tarkastelemalla transsendenttista maastoa - mielikuvitus maasta, joka on laskettu sinistä ja kosinista:

julkinen luokka TranscendentalTerrain toteuttaa maasto {yksityinen kaksinkertainen alfa, beeta; public TranscendentalTerrain (kaksinkertainen alfa, kaksinkertainen beeta) {this.alfa = alfa; tämä beeta = beeta; } public double getAltitude (double i, double j) {return .5 + .5 * Math.sin (i * alfa) * Math.cos (j * beta); } public RGB getColor (double i, double j) {palauta uusi RGB (.5 + .5 * Math.sin (i * alfa), .5 - .5 * Math.cos (j * beeta), 0.0); }} 

Rakentajamme hyväksyy kaksi arvoa, jotka määrittelevät maastomme taajuuden. Näitä käytetään laskemaan korkeudet ja värit Math.sin () ja Math.cos (). Muista, että nämä toiminnot palauttavat arvot -1,0 <= sin (), cos () <= 1,0, joten meidän on mukautettava palautusarvomme vastaavasti.

Fraktaalimaastot

Yksinkertaiset matemaattiset maastot eivät ole hauskaa. Haluamme jotain, joka näyttää ainakin hyväksyttävän todelliselta. Voisimme käyttää todellisia topografiatiedostoja maastokarttamme (esimerkiksi San Franciscon lahti tai Marsin pinta). Vaikka tämä on helppoa ja käytännöllistä, se on hieman tylsää. Tarkoitan, että olemme

ollut

siellä. Haluamme todella jotain, joka näyttää kelvolliselta todelliselta

ja

ei ole koskaan ennen nähty. Mene fraktaalien maailmaan.

Fraktaali on jotain (toiminto tai esine), joka näyttää itse samankaltaisuus. Esimerkiksi Mandelbrot-sarja on fraktaalitoiminto: jos suurennat Mandelbrot-sarjaa suuresti, löydät pieniä sisäisiä rakenteita, jotka muistuttavat itse Mandelbrot-pääosaa. Vuorijono on myös fraktaali, ainakin ulkonäöltään. Läheltä katsottuna yksittäisen vuoren pienet piirteet muistuttavat vuorijonon suuria ominaisuuksia, jopa yksittäisten lohkareiden karheuteen asti. Seuraamme tätä samankaltaisuuden periaatetta luodaksemme fraktaalimaastomme.

Pohjimmiltaan mitä teemme, on tuottaa karkea, alkuperäinen satunnainen maasto. Sitten lisätään rekursiivisesti muita satunnaisia ​​yksityiskohtia, jotka jäljittelevät kokonaisuuden rakennetta, mutta yhä pienemmissä mittakaavoissa. Todellisen käyttämämme algoritmin, Diamond-Square-algoritmin, kuvasivat alun perin Fournier, Fussell ja Carpenter vuonna 1982 (katso lisätietoja Resursseista).

Nämä ovat vaiheet, joiden läpi rakennamme fraktaalimaastomme:

  1. Määritämme ensin satunnaisen korkeuden ruudukon neljälle kulmapisteelle.

  2. Otetaan sitten näiden neljän kulman keskiarvo, lisätään satunnainen häiriö ja osoitetaan tämä ruudukon keskipisteelle (ii seuraavassa kaaviossa). Tätä kutsutaan timantti- vaihe, koska luomme timanttikuvion ristikkoon. (Ensimmäisessä iteraatiossa timantit eivät näytä timanteilta, koska ne ovat ristikon reunalla; mutta jos tarkastelet kaaviota, ymmärrät mitä pääsen.)

  3. Otetaan sitten jokainen tuottamamme timantti, keskitetään neljä kulmaa, lisätään satunnainen häiriö ja osoitetaan tämä timantin keskipisteelle (iii seuraavassa kaaviossa). Tätä kutsutaan neliö- vaihe, koska luomme neliön kuvion ruudukkoon.

  4. Seuraavaksi sovellamme timanttivaihetta uudelleen jokaiseen neliöaskeleeseen, jonka luomme neliöaskeleessa, ja levitä sitten neliö- astu jokaiselle timantille, jonka loimme timanttivaiheessa, ja niin edelleen, kunnes ristikkomme on riittävän tiheä.

Esiin tulee ilmeinen kysymys: Kuinka paljon häiritsemme verkkoa? Vastaus on, että aloitamme karheuskertoimella 0,0 <karheus <1,0. Iteraatiolla n Diamond-Square-algoritmistamme lisätään satunnainen häiriö ruudukkoon: -läpäisevyys n = = häiriö <= karheus n. Pohjimmiltaan, kun lisäämme hienompia yksityiskohtia ruudukkoon, pienennämme tekemiemme muutosten laajuutta. Pienet muutokset pienessä mittakaavassa muistuttavat suuressa mittakaavassa suuria muutoksia.

Jos valitsemme pienen arvon karheus, niin maastomme on hyvin sileä - muutokset pienenevät nopeasti nollaan. Jos valitsemme suuren arvon, maasto on hyvin karkea, koska muutokset pysyvät merkittävinä pienillä verkko-osastoilla.

Tässä on koodi fraktaalikarttamme toteuttamiseksi:

julkinen luokka FractalTerrain toteuttaa maasto {yksityinen kaksinkertainen [] [] maasto; yksityinen kaksinkertainen karheus, min, max; yksityiset osastot; yksityinen Random rng; julkinen FractalTerrain (int lod, kaksinkertainen karheus) {tämä.läpäisevyys = karheus; tämä.jako = 1 << lod; maasto = uusi kaksinkertainen [jako + 1] [jako + 1]; rng = uusi satunnainen (); maasto [0] [0] = rnd (); maasto [0] [jako] = rnd (); maasto [jako] [jako] = rnd (); maasto [jakaumat] [0] = rnd (); kaksinkertainen karheus = karheus; for (int i = 0; i <lod; ++ i) {int q = 1 << i, r = 1 <> 1; for (int j = 0; j <jako; j + = r) varten (int k = 0; k 0) varten (int j = 0; j <= jakot; j + = s) varten (int k = (j + s)% r; k <= jakoja; k + = r) neliö (j - s, k - s, r, karkea); karkea * = karheus; } min = max = maasto [0] [0]; for (int i = 0; i <= jako; ++ i) for (int j = 0; j <= jako; ++ j) if (maasto [i] [j] max) max = maasto [i] [ j]; } yksityinen tyhjä timantti (int x, int y, int-puoli, kaksinkertainen asteikko) {if (puoli> 1) {int puoli = sivu / 2; kaksinkertainen keskiarvo = (maasto [x] [y] + maasto [x + puoli] [y] + maasto [x + puoli] [y + puoli] + maasto [x] [y + puoli]) * 0,25; maasto [x + puoli] [y + puoli] = keskiarvo + rnd () * asteikko; }} yksityinen tyhjä neliö (int x, int y, int-puoli, kaksinkertainen asteikko) {int puoli = sivu / 2; kaksinkertainen keskiarvo = 0,0, summa = 0,0; jos (x> = 0) {keskiarvo + = maasto [x] [y + puoli]; summa + = 1,0; } if (y> = 0) {avg + = maasto [x + puoli] [y]; summa + = 1,0; } if (x + puoli <= jako) {avg + = maasto [x + puoli] [y + puoli]; summa + = 1,0; } if (y + puoli <= jako) {avg + = maasto [x + puoli] [y + puoli]; summa + = 1,0; } maasto [x + puoli] [y + puoli] = keskiarvo / summa + rnd () * asteikko; } yksityinen kaksinkertainen rnd () {return 2. * rng.nextDouble () - 1.0; } public double getAltitude (double i, double j) {double alt = maasto [(int) (i * jako)] [(int) (j * jako)]; paluu (alt - min) / (max - min); } yksityinen RGB sininen = uusi RGB (0,0, 0,0, 1,0); yksityinen RGB vihreä = uusi RGB (0,0, 1,0, 0,0); yksityinen RGB valkoinen = uusi RGB (1,0, 1,0, 1,0); public RGB getColor (double i, double j) {double a = getAltitude (i, j); jos (a <.5) palauttaa sinisen. lisää (vihreä. vähennä (sininen). asteikko ((a - 0.0) / 0.5)); muuten palaa vihreäksi. lisää (valkoinen. vähennä (vihreä). mittakaava ((a - 0,5) / 0,5)); }} 

Konstruktorissa määritämme molemmat karheuskertoimen karheus ja yksityiskohtien taso lod. Yksityiskohtaisuustaso on suoritettavien iteraatioiden määrä - tietyn tason n, tuotamme ruudukon (2n + 1 x 2n + 1) näytteet. Kullekin iteraatiolle levitetään timanttivaihe jokaiseen ruudukon neliöön ja sitten neliön vaihe jokaiseen timanttiin. Sen jälkeen lasketaan näytteen vähimmäis- ja enimmäisarvot, joita käytämme maaston korkeuksien skaalaamiseen.

Pisteen korkeuden laskemiseksi skaalataan ja palautetaan lähin ruudukkonäyte pyydettyyn paikkaan. Ihannetapauksessa interpoloimme itse asiassa ympäröivien näytepisteiden välillä, mutta tämä menetelmä on yksinkertaisempi ja tarpeeksi hyvä tässä vaiheessa. Lopullisessa sovelluksessamme tätä ongelmaa ei esiinny, koska sovitamme tosiasiallisesti paikat, joista otamme maaston, pyytämäämme yksityiskohtiin. Maaston värittämiseksi palautamme yksinkertaisesti arvon sinisen, vihreän ja valkoisen välillä näytepisteen korkeudesta riippuen.

Tessellating maastomme

Meillä on nyt maastokartta, joka on määritetty neliöalueelle. Meidän on päätettävä, miten aiomme todella vetää tämän ruudulle. Voisimme ampua säteitä maailmaan ja yrittää selvittää, mihin maastoon ne osuvat, kuten teimme edellisessä artikkelissa. Tämä lähestymistapa olisi kuitenkin erittäin hidas. Sen sijaan teemme sen, että arvioimme sileän maaston joukolla yhdistettyjä kolmioita - eli tesselloimme maastomme.

Tessellaatti: muodostaa tai koristella mosaiikilla (latinasta tessellatus).

Kolmioverkon muodostamiseksi otamme maastomme tasaisesti säännölliseen ristikkoon ja peitämme tämän ruudukon kolmioilla - kaksi kullekin ruudukon neliölle. On monia mielenkiintoisia tekniikoita, joita voisimme käyttää yksinkertaistamaan tätä kolmioverkkoa, mutta tarvitsemme niitä vain, jos nopeus olisi huolestuttava.

Seuraava koodinpätkä täyttää maastoruudukon elementit fraktaalimaastotiedoilla. Pienennämme maastomme pystysuoraa akselia alaspäin, jotta korkeudet ovat hieman vähemmän liioiteltuja.

kaksinkertainen liioittelu =, 7; int lod = 5; int vaiheet = 1 << lod; Kolminkertainen [] kartta = uusi Kolminkertainen [vaiheet + 1] [vaiheet + 1]; Kolminkertaiset [] värit = uusi RGB [vaiheet + 1] [vaiheet + 1]; Maasto maasto = uusi FractalTerrain (lod, .5); for (int i = 0; i <= vaiheet; ++ i) {for (int j = 0; j <= vaiheet; ++ j) {kaksinkertainen x = 1,0 * i / askel, z = 1,0 * j / askel ; kaksinkertainen korkeus = maasto.getAltitude (x, z); kartta [i] [j] = uusi kolminkertainen (x, korkeus * liioittelu, z); värit [i] [j] = maasto.getColor (x, z); }} 

Saatat kysyä itseltäsi: Miksi siis kolmioita eikä neliöitä? Ruudukon neliöiden käytössä on ongelma, että ne eivät ole tasaisia ​​3D-tilassa. Jos otat huomioon neljä satunnaista pistettä avaruudessa, on erittäin epätodennäköistä, että ne ovat samantasoisia. Joten sen sijaan hajotamme maastomme kolmioiksi, koska voimme taata, että kaikki kolme avaruuspistettä ovat samantasoisia. Tämä tarkoittaa sitä, että maastossa, jota päätämme piirtää, ei ole aukkoja.