Programmēšana

3D grafiskā Java: atveidojiet fraktāļu ainavas

3D datorgrafikai ir daudz lietojumu - sākot no spēlēm līdz datu vizualizācijai, virtuālajai realitātei un citur. Biežāk ātrumam ir galvenā nozīme, padarot darbu paveiktu ar specializētu programmatūru un aparatūru. Īpaša mērķa grafikas bibliotēkas nodrošina augsta līmeņa API, taču slēpj, kā tiek veikts reālais darbs. Tā kā mēs esam programmētāji, kas darbojas pret metālu, tas mums nav pietiekami labs! Mēs noliksim API skapī un aizkulisēs apskatīsim, kā attēli tiek faktiski ģenerēti - sākot no virtuālā modeļa definīcijas līdz tā faktiskajai renderēšanai uz ekrāna.

Mēs aplūkosim diezgan specifisku tēmu: izveido un renderē reljefa kartes, piemēram, Marsa virsmu vai dažus zelta atomus. Reljefa kartes renderēšanu var izmantot ne tikai estētiskiem mērķiem - daudzi datu vizualizācijas paņēmieni rada datus, kurus var atveidot kā reljefa kartes. Mani nodomi, protams, ir pilnīgi mākslinieciski, kā jūs varat redzēt zemāk esošajā attēlā! Ja vēlaties, kods, kuru mēs izveidosim, ir pietiekami vispārīgs, lai tikai ar nelielu pielāgošanu to varētu izmantot arī 3D struktūru, izņemot reljefu, atveidošanai.

Noklikšķiniet šeit, lai apskatītu un apstrādātu apvidus sīklietotni.

Gatavojoties mūsu šodienas diskusijai, iesaku izlasīt jūnija “Zīmēt teksturētas sfēras”, ja vēl neesat to izdarījis. Raksts parāda staru izsekošanas pieeju attēlu atveidošanai (staru raidīšana virtuālā ainā, lai iegūtu attēlu). Šajā rakstā ainas elementi tiks parādīti tieši displejā. Lai gan mēs izmantojam divas dažādas metodes, pirmajā rakstā ir daži pamatmateriāli par java.awt.image pakete, kuru es šajā diskusijā nepārmeklēšu.

Apvidus kartes

Sāksim ar definīciju a

reljefa karte

. Reljefa karte ir funkcija, kas kartē 2D koordinātas

(x, y)

līdz augstumam

a

un krāsa

c

. Citiem vārdiem sakot, reljefa karte ir vienkārši funkcija, kas apraksta nelielas teritorijas topogrāfiju.

Definēsim savu reljefu kā saskarni:

publiskā saskarne Terrain {public double getAltitude (double i, double j); publiskā RGB getColor (dubultā i, dubultā j); } 

Šī raksta nolūkā mēs to pieņemsim 0,0 <= i, j, augstums <= 1,0. Tā nav prasība, taču tā mums dos labu ideju, kur atrast apvidu, kuru mēs apskatīsim.

Mūsu reljefa krāsa ir aprakstīta vienkārši kā RGB triplets. Lai izveidotu interesantākus attēlus, mēs varētu apsvērt iespēju pievienot citu informāciju, piemēram, virsmas spīdumu utt. Pagaidām tomēr rīkosies šāda klase:

publiskā klase RGB {privāts dubultā r, g, b; publiskais RGB (dubultā r, dubultā g, dubultā b) {this.r = r; tas.g = g; šī.b = b; } public RGB add (RGB rgb) {atgriezt jaunu RGB (r + rgb.r, g + rgb.g, b + rgb.b); } publiskā RGB atņemšana (RGB rgb) {atgriež jaunu RGB (r - rgb.r, g - rgb.g, b - rgb.b); } publiskā RGB skala (dubultā skala) {atgriež jaunu RGB (r * skala, g * skala, b * skala); } private int toInt (dubultvērtība) {return (vērtība 1.0)? 255: (int) (vērtība * 255,0); } public int toRGB () toInt (b); } 

The RGB klase nosaka vienkāršu krāsu konteineru. Mēs piedāvājam dažas pamata iespējas krāsu aritmētiskai veikšanai un peldošā komata krāsas pārveidošanai iesaiņota vesela skaitļa formātā.

Pārpasaulīgi reljefi

Mēs sāksim aplūkot pārpasaulīgu reljefu - fantāzija par reljefu, kas aprēķināts no sinusa un kosinusa:

publiskā klase TranscendentalTerrain īsteno Terrain {private double alfa, beta; public TranscendentalTerrain (dubultā alfa, dubultā beta) {this.alfa = alfa; šī.beta = beta; } 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) {atgriež jaunu RGB (.5 + .5 * Math.sin (i * alfa), .5 - .5 * Math.cos (j * beta), 0.0); }} 

Mūsu konstruktors pieņem divas vērtības, kas nosaka mūsu reljefa biežumu. Mēs tos izmantojam, lai aprēķinātu augstumus un krāsas, izmantojot Math.sin () un Math.cos (). Atcerieties, ka šīs funkcijas atgriež vērtības -1,0 <= grēks (), cos () <= 1,0, tāpēc mums attiecīgi jāpielāgo atgriešanās vērtības.

Fraktāļu reljefi

Vienkārši matemātiski reljefi nav jautri. Mēs vēlamies kaut ko tādu, kas izskatās vismaz pieņemami reāls. Kā reljefa karti mēs varētu izmantot reālus topogrāfijas failus (piemēram, Sanfrancisko līci vai Marsa virsmu). Lai gan tas ir viegli un praktiski, tas ir nedaudz blāvi. Es domāju, ka esam

bijis

tur. Tas, ko mēs patiešām vēlamies, ir kaut kas, kas izskatās pietiekami reāls

un

vēl nekad nav redzēts. Ienāc fraktāļu pasaulē.

Fraktāls ir kaut kas (funkcija vai objekts), kas eksponē sevis līdzība. Piemēram, Mandelbrota komplekts ir fraktāla funkcija: ja jūs ievērojami palielināsiet Mandelbrota komplektu, jūs atradīsit sīkas iekšējās struktūras, kas atgādina pašu galveno Mandelbrotu. Arī kalnu grēda ir fraktāla, vismaz pēc izskata. No tuvplāna nelielas atsevišķa kalna iezīmes līdz pat atsevišķu laukakmeņu raupjumam līdzinās lielām kalnu grēdas iezīmēm. Mēs ievērosim šo sevis līdzības principu, lai radītu mūsu fraktālo reljefu.

Būtībā tas, ko mēs darīsim, radīs rupju, sākotnēju nejaušu reljefu. Tad rekursīvi pievienosim papildu izlases detaļas, kas atdarina visa struktūru, bet arvien mazākās skalās. Faktisko algoritmu, ko mēs izmantosim, Diamond-Square algoritmu sākotnēji aprakstīja Fournier, Fussell un Carpenter 1982. gadā (sīkāku informāciju skatiet resursos).

Šīs ir darbības, kuras mēs veiksim, lai izveidotu savu fraktālo reljefu:

  1. Vispirms četriem režģa stūra punktiem piešķiram nejaušu augstumu.

  2. Pēc tam mēs ņemam šo četru stūru vidējo rādītāju, pievienojam nejaušu traucējumu un piešķiram to tīkla viduspunktam (ii šādā diagrammā). To sauc par dimants solis, jo mēs izveidojam dimanta modeli uz režģa. (Pirmajā atkārtojumā dimanti neizskatās pēc dimantiem, jo ​​tie atrodas režģa malā; bet, ja paskatās uz diagrammu, jūs sapratīsit, pie kā es nokļūstu.)

  3. Pēc tam mēs ņemam katru no mūsu ražotajiem dimantiem, vidēji vērtējam četrus stūrus, pievienojam nejaušu traucējumu un piešķiram to dimanta viduspunktam (iii šādā diagrammā). To sauc par kvadrāts solis, jo mēs izveidojam kvadrātveida modeli uz režģa.

  4. Pēc tam katram kvadrātam, kuru izveidojām kvadrātveida pakāpē, mēs atkal uzklājam dimanta soli, pēc tam atkal uzliekam kvadrāts solis līdz katram dimantam, ko izveidojām dimanta pakāpienā, un tā tālāk, līdz mūsu režģis ir pietiekami blīvs.

Rodas acīmredzams jautājums: Cik daudz mēs traucējam tīklu? Atbilde ir tāda, ka mēs sākam ar raupjuma koeficientu 0,0 <raupjums <1,0. Atkārtojot n no mūsu Dimanta kvadrāta algoritma, mēs režģim pievienojam nejaušu traucējumu: -izturīban <= perturbācija <= raupjumsn. Būtībā, pievienojot režģim sīkākas detaļas, mēs samazinām veikto izmaiņu mērogu. Nelielas izmaiņas nelielā mērogā ir frakcionāli līdzīgas lielām izmaiņām lielākā mērogā.

Ja mēs izvēlamies nelielu vērtību raupjums, tad mūsu reljefs būs ļoti gluds - izmaiņas ļoti ātri samazināsies līdz nullei. Ja mēs izvēlēsimies lielu vērtību, tad reljefs būs ļoti nelīdzens, jo izmaiņas paliek nozīmīgas nelielos tīkla sadalījumos.

Šeit ir kods, lai ieviestu mūsu fraktālo reljefa karti:

publiskā klase FractalTerrain īsteno Terrain {private double [] [] reljefu; privāta dubultā raupjums, min, max; privātā nodalījumi; privāts Random rng; public FractalTerrain (int lod, dubultā raupjums) {this.caurums = raupjums; tas.dalījumi = 1 << lod; reljefs = jauns dubultā [dalījums + 1] [dalījums + 1]; rng = jauns Nejaušs (); reljefs [0] [0] = rnd (); reljefs [0] [dalījumi] = rnd (); reljefs [dalījumi] [dalījumi] = rnd (); reljefs [dalījumi] [0] = rnd (); divkāršs raupjums = raupjums; par (int i = 0; i <lod; ++ i) {int q = 1 << i, r = 1 <> 1; par (int j = 0; j <dalījumi; j + = r) par (int k = 0; k 0) par (int j = 0; j <= dalījumi; j + = s) par (int k = (j + s)% r; k <= dalījumi; k + = r) kvadrāts (j - s, k - s, r, aptuvens); raupja * = raupjums; } min = max = reljefs [0] [0]; par (int i = 0; i <= dalījumi; ++ i) par (int j = 0; j <= dalījumi; ++ j) ja (reljefs [i] [j] max) max = reljefs [i] [ j]; } privāts tukšs dimants (int x, int y, int puse, dubultā skala) {if (puse> 1) {int puse = puse / 2; dubultā vid. = (reljefs [x] [y] + reljefs [x + puse] [y] + reljefs [x + puse] [y + puse] + reljefs [x] [y + puse]) * 0,25; reljefs [x + puse] [y + puse] = vid. + rnd () * skala; }} privāts tukšuma kvadrāts (int x, int y, int puse, dubultā skala) {int puse = sāns / 2; dubultā vidējā = 0,0, summa = 0,0; ja (x> = 0) {avg + = reljefs [x] [y + puse]; summa + = 1,0; } if (y> = 0) {avg + = reljefs [x + puse] [y]; summa + = 1,0; } if (x + puse <= dalījumi) {avg + = reljefs [x + puse] [y + puse]; summa + = 1,0; } ja (y + puse <= dalījumi) {avg + = reljefs [x + puse] [y + puse]; summa + = 1,0; } reljefs [x + puse] [y + puse] = vid. / summa + rnd () * skala; } private double rnd () {return 2. * rng.nextDouble () - 1,0; } public double getAltitude (double i, double j) {double alt = reljefs [(int) (i * dalījumi)] [(int) (j * dalījumi)]; atgriešanās (alt - min) / (max - min); } privāts RGB zils = jauns RGB (0,0, 0,0, 1,0); privāts RGB zaļš = jauns RGB (0,0, 1,0, 0,0); privāts RGB balts = jauns RGB (1,0, 1,0, 1,0); public RGB getColor (double i, double j) {double a = getAltitude (i, j); ja (a <.5) atgriežas zilā krāsā. pievienot (zaļš. atņemt (zils). mērogs ((a - 0.0) / 0.5)); vēl atgriezties green.add (white.subtract (green) .scale ((a - 0.5) / 0.5)); }} 

Konstruktorā mēs norādām gan raupjuma koeficientu raupjums un detalizācijas pakāpe lod. Detalizācijas pakāpe ir veicamo atkārtojumu skaits - detalizācijas pakāpei n, mēs ražojam režģi (2n + 1 x 2n + 1) paraugi. Katrai iterācijai mēs pielietojam dimanta pakāpienu katram kvadrātam režģī un pēc tam kvadrātveida soli katram dimantam. Pēc tam mēs aprēķinām minimālās un maksimālās parauga vērtības, kuras izmantosim, lai mērogotu mūsu reljefa augstumus.

Lai aprēķinātu punkta augstumu, mēs mērogojam un atgriežam tuvākais režģa paraugu uz pieprasīto vietu. Ideālā gadījumā mēs faktiski interpolētu starp apkārtējiem izlases punktiem, taču šī metode šajā brīdī ir vienkāršāka un pietiekami laba. Mūsu galīgajā pieteikumā šis jautājums neradīsies, jo mēs faktiski saskaņosim vietas, kur mēs ņemam paraugu no reljefa, ar pieprasīto detalizācijas pakāpi. Lai iekrāsotu mūsu reljefu, mēs vienkārši atgriežam vērtību starp zilu, zaļu un baltu, atkarībā no parauga punkta augstuma.

Tessellating mūsu reljefu

Tagad mums ir reljefa karte, kas definēta kvadrātveida domēnā. Mums jāizlemj, kā mēs to patiesībā uzzīmēsim uz ekrāna. Mēs varētu izšaut starus pasaulē un mēģināt noteikt, kuru reljefa daļu viņi skar, kā mēs to darījām iepriekšējā rakstā. Šī pieeja tomēr būtu ārkārtīgi lēna. Tas, ko mēs darīsim tā vietā, ir aptuvens vienmērīgais reljefs ar virkni savienotu trijstūru - tas ir, mēs sadalīsim savu reljefu.

Tessellate: veidoties vai rotāt mozaīku (no latīņu tessellatus).

Lai izveidotu trīsstūra sietu, mēs vienmērīgi ņemsim mūsu reljefu parastā režģī un pēc tam pārklājiet šo režģi ar trīsstūriem - pa diviem katram režģa kvadrātam. Ir daudz interesantu paņēmienu, kurus mēs varētu izmantot, lai vienkāršotu šo trijstūra sietu, taču tie mums būtu nepieciešami tikai tad, ja bažas sagādā ātrums.

Šis koda fragments aizpilda mūsu reljefa režģa elementus ar fraktālā reljefa datiem. Mēs samazinām reljefa vertikālo asi uz leju, lai augstumi būtu nedaudz mazāk pārspīlēti.

divkāršs pārspīlējums =, 7; int lod = 5; int soļi = 1 << lod; Trīskārša [] karte = jauna Trīskārša [pakāpieni + 1] [pakāpieni + 1]; Trīskāršas [] krāsas = jauns RGB [soļi + 1] [soļi + 1]; Reljefa reljefs = jauns FractalTerrain (lod, .5); for (int i = 0; i <= soļi; ++ i) {par (int j = 0; j <= soļi; ++ j) {dubultā x = 1,0 * i / soļi, z = 1,0 * j / soļi ; dubultā augstums = terrain.getAltitude (x, z); karte [i] [j] = jauns trīskāršais (x, augstums * pārspīlēts, z); krāsas [i] [j] = reljefs.getColor (x, z); }} 

Iespējams, jūs sev jautājat: kāpēc tad trīsstūri, nevis kvadrāti? Režģa kvadrātu izmantošanas problēma ir tā, ka 3D telpā tie nav plakani. Ja ņemat vērā četrus nejaušus punktus telpā, ir maz ticams, ka tie būs kopīgi. Tāpēc tā vietā mēs sadalām savu reljefu līdz trijstūriem, jo ​​mēs varam garantēt, ka visi trīs vietas kosmosā būs kopīgi. Tas nozīmē, ka reljefā, kuru mēs uzzīmēsim, nebūs atstarpju.

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