Programiranje

3D grafična Java: upodablja fraktalne pokrajine

3D računalniška grafika ima veliko uporab - od iger do vizualizacije podatkov, navidezne resničnosti in še več. Pogosto je hitrost izjemnega pomena, zato je za opravljanje dela potrebna posebna programska in strojna oprema. Grafične knjižnice za posebne namene ponujajo API na visoki ravni, vendar skrivajo, kako je resnično opravljeno. Kot programerji od nosu do kovine pa to ni dovolj dobro za nas! API bomo postavili v omarico in si v zakulisju ogledali, kako se slike dejansko generirajo - od definicije navideznega modela do njegovega dejanskega upodabljanja na zaslon.

Ogledali si bomo dokaj specifično temo: ustvarjanje in upodabljanje terenskih zemljevidov, kot je površje Marsa ali nekaj atomov zlata. Prikazovanje terenskih zemljevidov je mogoče uporabiti za več kot le estetske namene - številne tehnike vizualizacije podatkov ustvarjajo podatke, ki jih je mogoče upodabljati kot zemljevide terena. Moji nameni so seveda povsem umetniški, kot lahko vidite na spodnji sliki! Če želite, je koda, ki jo bomo izdelali, dovolj splošna, da jo lahko z le manjšim popravkom uporabimo tudi za upodabljanje 3D struktur, ki niso tereni.

Kliknite tukaj, da si ogledate in manipulirate s programom terena.

V pripravah na današnjo razpravo predlagam, da preberete junijsko "Risanje teksturiranih krogel", če tega še niste storili. Članek prikazuje pristop sledenja žarkom pri upodabljanju slik (sprožanje žarkov v navidezno sceno za ustvarjanje slike). V tem članku bomo upodabljali elemente scene neposredno na zaslon. Čeprav uporabljamo dve različni tehniki, prvi članek vsebuje nekaj osnovnega gradiva o java.awt.image paket, ki ga v tej razpravi ne bom ponavljal.

Zemljevidi terena

Začnimo z opredelitvijo a

zemljevid terena

. Zemljevid terena je funkcija, ki preslika 2D koordinate

(x, y)

do višine

a

in barva

c

. Z drugimi besedami, karta terena je preprosto funkcija, ki opisuje topografijo majhnega območja.

Določimo svoj teren kot vmesnik:

javni vmesnik Terrain {javni dvojni getAltitude (dvojni i, dvojni j); javni RGB getColor (dvojni i, dvojni j); } 

Za namen tega članka bomo to domnevali 0,0 <= i, j, nadmorska višina <= 1,0. To ni pogoj, vendar nam bo dala dobro idejo, kje najti teren, ki si ga bomo ogledali.

Barva našega terena je opisana preprosto kot RGB trojka. Če želite ustvariti bolj zanimive slike, lahko razmislimo o dodajanju drugih informacij, kot je sijaj površine itd. Za zdaj pa bo to storil naslednji razred:

javni razred RGB {zasebni dvojnik r, g, b; javni RGB (dvojni r, dvojni g, dvojni b) {this.r = r; to.g = g; to.b = b; } javni dodatek RGB (RGB rgb) {vrni nov RGB (r + rgb.r, g + rgb.g, b + rgb.b); } javno odštevanje RGB (RGB rgb) {vrni novo RGB (r - rgb.r, g - rgb.g, b - rgb.b); } javna lestvica RGB (dvojna lestvica) {vrni novo RGB (lestvica r *, lestvica g *, lestvica b *); } private int toInt (dvojna vrednost) {return (vrednost 1.0)? 255: (int) (vrednost * 255,0); } javni int toRGB () toInt (b); } 

The RGB razred definira preprost barvni vsebnik. Ponujamo nekaj osnovnih zmogljivosti za izvajanje barvne aritmetike in pretvorbo barve s plavajočo vejico v celopakirani format.

Transcendentalni tereni

Začeli bomo s pregledom transcendentalnega terena - fancyspeak za teren, izračunan iz sinusov in kosinusov:

javni razred TranscendentalTerrain izvaja Terrain {zasebna dvojna alfa, beta; javni TranscendentalTerrain (dvojna alfa, dvojna beta) {this.alpha = alfa; this.beta = beta; } public double getAltitude (double i, double j) {return .5 + .5 * Math.sin (i * alpha) * Math.cos (j * beta); } javni RGB getColor (dvojni i, dvojni j) {vrni nov RGB (.5 + .5 * Math.sin (i * alpha), .5 - .5 * Math.cos (j * beta), 0.0); }} 

Naš konstruktor sprejema dve vrednosti, ki določata pogostost našega terena. Te uporabljamo za izračun nadmorske višine in barv Math.sin () in Math.cos (). Ne pozabite, da te funkcije vrnejo vrednosti -1,0 <= sin (), cos () <= 1,0, zato moramo svoje povratne vrednosti ustrezno prilagoditi.

Fraktalni tereni

Preprosti matematični tereni niso zabavni. Kar hočemo, je nekaj, kar je videti vsaj občasno resnično. Za zemljevid terena bi lahko uporabili prave topografske datoteke (na primer zaliv San Francisco ali Marsovo površje). Čeprav je to enostavno in praktično, je nekoliko dolgočasno. Mislim, že smo

bila

tam. Zares si želimo nekaj, kar je videti resnično resnično

in

še nikoli ni bil viden. Vstopite v svet fraktalov.

Fraktal je nekaj (funkcija ali predmet), ki razstavlja samopodobnost. Nabor Mandelbrot je na primer fraktalna funkcija: če močno povečate niz Mandelbrot, boste našli drobne notranje strukture, ki spominjajo na glavnega Mandelbrota. Tudi gorska veriga je vsaj na videz fraktalna. Od blizu so majhne značilnosti posamezne gore podobne velikim značilnostim gorskega območja, celo do hrapavosti posameznih balvanov. Temu načelu samopodobe bomo sledili, da bomo ustvarili svoje fraktalne terene.

V bistvu bomo naredili grob, začetni naključni teren. Nato bomo rekurzivno dodali dodatne naključne podrobnosti, ki posnemajo strukturo celote, vendar na vedno manjših merilih. Dejanski algoritem, ki ga bomo uporabili, algoritem Diamond-Square, so prvotno opisali Fournier, Fussell in Carpenter leta 1982 (za podrobnosti glejte Vire).

To so koraki, ki jih bomo naredili za izgradnjo našega fraktalnega terena:

  1. Najprej dodelimo naključno višino štirim vogalnim točkam mreže.

  2. Nato vzamemo povprečje teh štirih vogalov, dodamo naključno motenje in to dodelimo srednji točki mreže (ii v naslednjem diagramu). To se imenuje diamant korak, ker na mreži ustvarjamo diamantni vzorec. (Na prvi ponovitvi diamanti niso videti kot diamanti, ker so na robu mreže; če pa pogledate diagram, boste razumeli, na kaj grem.)

  3. Nato vzamemo vsak diamant, ki smo ga izdelali, povprečimo štiri vogale, dodamo naključno motenje in to dodelimo sredini diamanta (iii v naslednjem diagramu). To se imenuje kvadrat korak, ker na mreži ustvarjamo kvadratni vzorec.

  4. Nato na vsak kvadrat, ki smo ga ustvarili v kvadratnem koraku, znova uporabimo diamantni korak, nato znova uporabimo kvadrat stopimo do vsakega diamanta, ki smo ga ustvarili v diamantnem koraku, in tako naprej, dokler naša mreža ni dovolj gosta.

Pojavi se očitno vprašanje: koliko zmotimo mrežo? Odgovor je, da začnemo s koeficientom hrapavosti 0,0 <hrapavost <1,0. Ob ponovitvi n našega algoritma Diamond-Square mreži dodamo naključno motenje: -hrapavostn <= motnja <= hrapavostn. Ko mreži dodamo natančnejše podrobnosti, zmanjšamo obseg sprememb, ki jih naredimo. Majhne spremembe v majhnem obsegu so fraktalno podobne velikim spremembam v večjem obsegu.

Če izberemo majhno vrednost za hrapavost, potem bo naš teren zelo gladek - spremembe se bodo zelo hitro zmanjšale na nič. Če izberemo veliko vrednost, bo teren zelo razgiban, saj so spremembe pri majhnih omrežnih razdelitvah še vedno pomembne.

Tu je koda za izvedbo našega fraktalnega zemljevida terena:

javni razred FractalTerrain izvaja Terrain {zasebni dvojni [] [] teren; zasebna dvojna hrapavost, min, max; zasebni oddelki int; zasebno naključno rng; javni FractalTerrain (int lod, dvojna hrapavost) {this.oughness = hrapavost; this.divisions = 1 << lod; teren = nov dvojnik [divizije + 1] [divizije + 1]; rng = novo naključno (); teren [0] [0] = rnd (); teren [0] [delitve] = rnd (); teren [delitve] [delitve] = rnd (); teren [delitve] [0] = rnd (); dvojna hrapavost = hrapavost; za (int i = 0; i <lod; ++ i) {int q = 1 << i, r = 1 <> 1; za (int j = 0; j <delitve; j + = r) za (int k = 0; k 0) za (int j = 0; j <= delitve; j + = s) za (int k = (j + s)% r; k <= delitve; k + = r) kvadrat (j - s, k - s, r, grobo); hrapavost * = hrapavost; } min = največ = teren [0] [0]; for (int i = 0; i <= divizije; ++ i) for (int j = 0; j <= divizije; ++ j) if (terrain [i] [j] max) max = terrain [i] [ j]; } zasebni prazninski diamant (int x, int y, int stran, dvojna lestvica) {if (stran> 1) {int polovica = stran / 2; dvojno povprečje = (teren [x] [y] + teren [x + stran] [y] + teren [x + stran] [y + stran] + teren [x] [y + stran]) * 0,25; teren [x + polovica] [y + polovica] = povprečno + rnd () * lestvica; }} zasebni void kvadrat (int x, int y, int stran, dvojna lestvica) {int polovica = stran / 2; dvojno povprečje = 0,0, vsota = 0,0; if (x> = 0) {avg + = teren [x] [y + polovica]; vsota + = 1,0; } if (y> = 0) {avg + = teren [x + polovica] [y]; vsota + = 1,0; } if (x + stran <= delitve) {avg + = teren [x + stran] [y + polovica]; vsota + = 1,0; } if (y + stran <= delitve) {povprečje + = teren [x + polovica] [y + stran]; vsota + = 1,0; } teren [x + polovica] [y + polovica] = povprečno / vsota + rnd () * lestvica; } private double rnd () {return 2. * rng.nextDouble () - 1,0; } public double getAltitude (dvojni i, dvojni j) {dvojni alt = teren [(int) (i * divizije)] [(int) (j * divizije)]; povratek (alt - min) / (max - min); } zasebno RGB modro = novo RGB (0,0, 0,0, 1,0); zasebno RGB zeleno = novo RGB (0,0, 1,0, 0,0); zasebno RGB belo = novo RGB (1.0, 1.0, 1.0); javni RGB getColor (dvojni i, dvojni j) {dvojni a = getAltitude (i, j); če (a <.5) vrne blue.add (green.subtract (blue) .scale ((a - 0.0) / 0.5)); sicer vrni green.add (white.subtract (green) .scale ((a - 0.5) / 0.5)); }} 

V konstruktorju določimo koeficient hrapavosti hrapavost in raven podrobnosti lod. Raven podrobnosti je število ponovitev, ki jih je treba izvesti - za raven podrobnosti n, izdelamo mrežo (2n + 1 x 2n + 1) vzorcev. Za vsako ponovitev uporabimo diamantni korak na vsak kvadrat v mreži in nato kvadratni korak na vsak diamant. Nato izračunamo najmanjšo in največjo vrednost vzorca, ki jo bomo uporabili za merjenje višine terena.

Za izračun nadmorske višine točke merimo in vrnemo najbližje vzorec mreže na zahtevano lokacijo. V idealnem primeru bi dejansko interpolirali med okoliškimi vzorčnimi točkami, vendar je ta metoda na tem mestu preprostejša in dovolj dobra. V naši končni prijavi se ta težava ne bo pojavila, ker bomo dejansko uskladili lokacije, kjer vzorčimo teren, do stopnje podrobnosti, ki jo zahtevamo. Za obarvanje terena preprosto vrnemo vrednost med modro, zeleno in belo, odvisno od nadmorske višine točke vzorčenja.

Tesaliranje našega terena

Zdaj imamo zemljevid terena definiran na kvadratni domeni. Odločiti se moramo, kako bomo to dejansko risali na zaslon. Lahko bi sprožili žarke v svet in poskušali ugotoviti, na kateri del terena udarijo, kot smo storili v prejšnjem članku. Vendar bi bil ta pristop izredno počasen. Namesto tega bomo približali gladki teren s kopico povezanih trikotnikov - to pomeni, da bomo svoj tesen označili kot tesselato.

Tesselat: oblikovati v mozaik ali ga okrasiti (iz lat tessellatus).

Če želimo oblikovati mrežo trikotnika, bomo enakomerno vzorčili naš teren v običajno mrežo in nato mrežo pokrili s trikotniki - dva za vsak kvadrat mreže. Obstaja veliko zanimivih tehnik, ki bi jih lahko uporabili za poenostavitev te trikotne mreže, vendar bi jih potrebovali le, če bi hitrost skrbela.

Naslednji fragment kode zapolni elemente naše mreže terena s fraktalnimi podatki o terenu. Smerimo navpično os našega terena, da višine nekoliko manj pretiravamo.

dvojno pretiravanje = .7; int lod = 5; int koraki = 1 << lod; Zemljevid Triple [] = nov Triple [koraki + 1] [koraki + 1]; Trojne [] barve = novi RGB [koraki + 1] [koraki + 1]; Teren terena = nov FractalTerrain (lod, .5); for (int i = 0; i <= koraki; ++ i) {for (int j = 0; j <= koraki; ++ j) {dvojni x = 1,0 * i / koraki, z = 1,0 * j / koraki ; dvojna nadmorska višina = terrain.getAltitude (x, z); zemljevid [i] [j] = nov trojni (x, nadmorska višina * pretiravanje, z); barve [i] [j] = terrain.getColor (x, z); }} 

Morda se sprašujete: Zakaj torej trikotniki in ne kvadrati? Težava pri uporabi kvadratov mreže je, da v 3D prostoru niso ravno. Če upoštevate štiri naključne točke v vesolju, je zelo malo verjetno, da bodo enakomerne. Namesto tega svoj teren razstavimo na trikotnike, saj lahko zagotovimo, da bodo katere koli tri točke v vesolju enakomerne. To pomeni, da na terenu ne bo vrzeli, ki bi jih na koncu risali.

$config[zx-auto] not found$config[zx-overlay] not found