Programavimas

3D grafinė „Java“: pateikite fraktalų peizažus

3D kompiuterinė grafika turi daugybę paskirčių - nuo žaidimų iki duomenų vizualizavimo, virtualios realybės ir ne tik. Dažniausiai greitis yra ypač svarbus, todėl norint atlikti darbą būtina atlikti specializuotą programinę ir techninę įrangą. Specialios paskirties grafikos bibliotekos teikia aukšto lygio API, tačiau slepia, kaip atliekamas tikras darbas. Vis dėlto, kaip „metalo programuotojai“, tai nėra pakankamai gerai! Paguldysime API į spintą ir užkulisiuose apžvelgsime, kaip iš tikrųjų generuojami vaizdai - nuo virtualaus modelio apibrėžimo iki faktinio jo perteikimo ekrane.

Mes nagrinėsime gana specifinę temą: generuosime ir perteiksime reljefo žemėlapius, tokius kaip Marso paviršius ar keli aukso atomai. Vietovės žemėlapio atvaizdavimas gali būti naudojamas ne tik estetiniams tikslams - daugelis duomenų vizualizavimo metodų sukuria duomenis, kuriuos galima atvaizduoti kaip reljefo žemėlapius. Mano ketinimai, žinoma, yra visiškai meniški, kaip matote paveikslėlyje žemiau! Jei norite, jūsų sukurtas kodas yra pakankamai bendras, kad tik šiek tiek pakoregavus, jis taip pat gali būti naudojamas 3D struktūroms, išskyrus vietoves, pateikti.

Spustelėkite čia, jei norite peržiūrėti ir valdyti vietovės programėlę.

Ruošdamasi šiandieninei mūsų diskusijai siūlau perskaityti birželio mėn. „Nubrėžkite tekstūruotas sferas“, jei to dar nepadarėte. Straipsnyje parodytas spindulių sekimo būdas vaizdų atvaizdavimui (spindulių šaudymas į virtualią sceną, kad būtų sukurtas vaizdas). Šiame straipsnyje scenos elementus pateiksime tiesiai į ekraną. Nors mes naudojame dvi skirtingas technikas, pirmajame straipsnyje yra keletas pagrindinės medžiagos apie java.awt.vaizdas paketo, kurio šioje diskusijoje neperplanuosiu.

Vietovės žemėlapiai

Pradėkime apibrėždami a

reljefo žemėlapis

. Vietovės žemėlapis yra funkcija, atspindinti 2D koordinates

(x, y)

į aukštį

a

ir spalva

c

. Kitaip tariant, reljefo žemėlapis yra tiesiog funkcija, apibūdinanti nedidelio ploto topografiją.

Apibrėžkime savo reljefą kaip sąsają:

viešoji sąsaja Terrain {public double getAltitude (double i, double j); viešasis RGB getColor (dvigubas i, dvigubas j); } 

Šiame straipsnyje mes manysime, kad 0,0 <= i, j, aukštis <= 1,0. Tai nėra reikalavimas, tačiau suteiks mums gerą idėją, kur rasti vietovę, kurią žiūrėsime.

Mūsų reljefo spalva apibūdinama tiesiog kaip RGB tripletas. Norėdami pagaminti įdomesnius vaizdus, ​​galime apsvarstyti galimybę pridėti kitos informacijos, tokios kaip paviršiaus blizgesys ir kt. Tačiau kol kas tiks ši klasė:

viešosios klasės RGB {privatus dvigubas r, g, b; viešasis RGB (dvigubas r, dvigubas g, dvigubas b) {this.r = r; tai.g = g; tai.b = b; } public RGB add (RGB rgb) {grąžinti naują RGB (r + rgb.r, g + rgb.g, b + rgb.b); } viešasis RGB atimtis (RGB rgb) {grąžinti naują RGB (r - rgb.r, g - rgb.g, b - rgb.b); } viešoji RGB skalė (dviguba skalė) {grąžinti naują RGB (r * skalė, g * skalė, b * skalė); } private int toInt (dviguba reikšmė) {return (reikšmė 1.0)? 255: (int) (vertė * 255,0); } public int toRGB () toInt (b); } 

RGB klasė apibrėžia paprastą spalvų indą. Mes teikiame keletą pagrindinių galimybių atlikti spalvų aritmetiką ir konvertuoti slankiojo kablelio spalvą į supakuoto sveiko skaičiaus formatą.

Transcendentinės vietovės

Pradėsime nuo transcendentinio reljefo pažinimo - fantazijos už reljefą, apskaičiuotą iš sinusų ir kosinusų:

viešoji klasė „TranscendentalTerrain“ įgyvendina reljefą {privati ​​dviguba alfa, beta; public TranscendentalTerrain (dviguba alfa, dviguba beta) {this.alfa = alfa; tai.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) {grąžinti naują RGB (.5 + .5 * Math.sin (i * alfa), .5 - .5 * Math.cos (j * beta), 0.0); }} 

Mūsų konstruktorius priima dvi reikšmes, apibrėžiančias mūsų reljefo dažnį. Mes juos naudojame apskaičiuodami aukštį ir spalvas Math.sin () ir Math.cos (). Atminkite, kad šios funkcijos grąžina reikšmes -1,0 <= sin (), cos () <= 1,0, todėl turime atitinkamai pakoreguoti grąžinimo vertes.

Fraktaliniai reljefai

Paprasti matematiniai reljefai nėra įdomus. Mes norime kažko, kas bent jau atrodo realiai. Kaip savo reljefo žemėlapį galėtume naudoti tikrus topografijos failus (pavyzdžiui, San Francisko įlanka arba Marso paviršius). Nors tai lengva ir praktiška, tačiau šiek tiek nuobodu. Aš turiu omenyje

buvo

ten. Tai, ko mes iš tikrųjų norime, yra kažkas, kas atrodo priimtinai realu

ir

dar niekad nematė. Įeikite į fraktalų pasaulį.

Fraktalas yra kažkas (funkcija ar objektas), kuris rodo savęs panašumas. Pavyzdžiui, „Mandelbrot“ rinkinys yra fraktalo funkcija: jei labai padidinsite „Mandelbrot“ rinkinį, rasite mažų vidinių struktūrų, panašių į patį pagrindinį „Mandelbrot“. Kalnų grandinė taip pat yra fraktališka, bent jau išvaizda. Iš arti mažos atskiro kalno savybės primena didelius kalnų bruožus, net iki atskirų riedulių šiurkštumo. Mes laikysimės šio panašumo į save principo, kad sukurtume savo fraktalines vietoves.

Iš esmės tai, ką mes padarysime, bus susidaryti šiurkščią, pradinę atsitiktinę vietovę. Tada rekursyviai pridėsime papildomų atsitiktinių detalių, imituojančių visumos struktūrą, tačiau vis mažesnėmis skalėmis. Tikrąjį algoritmą, kurį naudosime, „Diamond-Square“ algoritmą, iš pradžių aprašė Fournier, Fussell ir Carpenter 1982 m. (Išsamesnės informacijos rasite šaltiniuose).

Tai yra žingsniai, kuriuos atliksime kurdami savo fraktalinį reljefą:

  1. Pirmiausia keturiems tinklelio kampiniams taškams priskiriame atsitiktinį aukštį.

  2. Tada paimame šių keturių kampų vidurkį, pridedame atsitiktinį sutrikimą ir priskiriame jį tinklo vidurio taškui (ii šioje diagramoje). Tai vadinama deimantas žingsnis, nes tinklelyje kuriame deimantinį raštą. (Pirmos kartos metu deimantai neatrodo kaip deimantai, nes jie yra tinklelio krašte; bet jei pažiūrėsite į schemą, suprasite, kuo aš užsiimu.)

  3. Tada paimame kiekvieną iš mūsų pagamintų deimantų, apskaičiuojame keturis kampus, pridedame atsitiktinį sutrikimą ir priskiriame tai deimanto vidurio taškui (iii šioje diagramoje). Tai vadinama aikštė žingsnis, nes tinklelyje kuriame kvadratinį raštą.

  4. Tada mes pritaikome deimantinį žingsnį kiekvienam kvadratui, kurį sukūrėme kvadratiniame žingsnyje, tada vėl pritaikome aikštė žingsnis prie kiekvieno deimanto, kurį sukūrėme deimanto žingsnyje, ir taip toliau, kol mūsų tinklelis bus pakankamai tankus.

Kyla akivaizdus klausimas: kiek mes trikdome tinklelį? Atsakymas yra tas, kad mes pradedame nuo šiurkštumo koeficiento 0,0 <šiurkštumas <1,0. Kartojant n deimantinio kvadrato algoritmo prie tinklelio pridedame atsitiktinį sutrikimą: -prasmumasn <= sutrikimas <= šiurkštumasn. Iš esmės, kai tinklelyje pridedame smulkesnių detalių, sumažiname atliekamų pakeitimų mastą. Maži pokyčiai nedideliu mastu yra truputį panašūs į didelius didesnio masto pokyčius.

Jei pasirenkame mažą vertę šiurkštumas, tada mūsų reljefas bus labai lygus - pokyčiai labai greitai sumažės iki nulio. Jei pasirenkame didelę vertę, reljefas bus labai šiurkštus, nes pokyčiai išlieka reikšmingi mažuose tinklo skaidymuose.

Štai kodas, skirtas įgyvendinti mūsų fraktalinio reljefo žemėlapį:

viešoji klasė „FractalTerrain“ įgyvendina reljefą {private double [] [] reljefas; privatus dvigubas šiurkštumas, min, max; privatūs padaliniai; privatus Atsitiktinis rng; public FractalTerrain (int lod, dvigubas šiurkštumas) {this.visumas = šiurkštumas; tai.dalijimai = 1 << lod; reljefas = naujas dvigubas [padalijimai + 1] [daliniai + 1]; rng = naujas atsitiktinis (); reljefas [0] [0] = rnd (); reljefas [0] [dalybos] = rnd (); reljefas [dalybos] [dalybos] = rnd (); reljefas [padalos] [0] = rnd (); dvigubas grubus = šiurkštumas; už (int i = 0; i <lod; ++ i) {int q = 1 << i, r = 1 <> 1; už (int j = 0; j <dalybos; j + = r) už (int k = 0; k 0) už (int j = 0; j <= dalybas; j + = s) už (int k = (j + s)% r; k <= dalybos; k + = r) kvadratas (j - s, k - s, r, grubus); šiurkštus * = šiurkštumas; } min = max = reljefas [0] [0]; for (int i = 0; i <= dalybos; ++ i) for (int j = 0; j <= dalybos; ++ j) if (reljefas [i] [j] max) max = reljefas [i] [ j]; } privatus tuščias deimantas (int x, int y, int pusė, dviguba skalė) {if (pusė> 1) {int pusė = pusė / 2; dviguba vidurkis = (reljefas [x] [y] + reljefas [x + pusė] [y] + reljefas [x + pusė] [y + pusė] + reljefas [x] [y + pusė]) * 0,25; reljefas [x + pusė] [y + pusė] = vid. + rnd () * skalė; }} privatus tuštumos kvadratas (int x, int y, int pusė, dviguba skalė) {int half = side / 2; dvigubas vidurkis = 0,0, suma = 0,0; if (x> = 0) {avg + = reljefas [x] [y + pusė]; suma + = 1,0; } if (y> = 0) {avg + = reljefas [x + pusė] [y]; suma + = 1,0; } if (x + kraštas <= dalybos) {avg + = reljefas [x + pusė] [y + pusė]; suma + = 1,0; } if (y + pusė <= dalybos) {avg + = vietovė [x + pusė] [y + pusė]; suma + = 1,0; } reljefas [x + pusė] [y + pusė] = vid. / suma + rnd () * skalė; } privatus dvigubas rnd () {return 2. * rng.nextDouble () - 1.0; } public double getAltitude (double i, double j) {double alt = reljefas [(int) (i * dalybos)] [(int) (j * dalybos)]; grįžimas (alt - min) / (max - min); } privatus RGB mėlynas = naujas RGB (0,0, 0,0, 1,0); privatus RGB žalias = naujas RGB (0,0, 1,0, 0,0); privatus RGB baltas = naujas RGB (1,0, 1,0, 1,0); viešasis RGB getColor (dvigubas i, dvigubas j) {dvigubas a = getAltitude (i, j); if (a <.5) return blue.add (green.subtract (blue) .scale ((a - 0.0) / 0.5)); dar grįžti žalia.add (balta. atimtis (žalia). mastelis ((a - 0,5) / 0,5)); }} 

Konstruktoriuje nurodome ir šiurkštumo koeficientą šiurkštumas ir detalumo lygis lod. Detalumo lygis yra pakartojimų, kuriuos reikia atlikti, skaičius n, mes gaminame tinklelį (2n + 1 x 2n + 1) pavyzdžiai. Kiekvienai iteracijai mes pritaikome deimanto pakopą kiekvienam kvadratui tinklelyje, o tada kvadratinį žingsnį - kiekvienam deimantui. Vėliau apskaičiuojame mažiausią ir maksimalią imties vertes, kurias naudosime mastelio aukštyje.

Norėdami apskaičiuoti taško aukštį, mes mastelį ir grąžinti arčiausiai tinklelio pavyzdį į prašomą vietą. Idealiu atveju mes iš tikrųjų interpoliuosime tarp aplinkinių imties taškų, tačiau šis metodas šiuo metu yra paprastesnis ir pakankamai geras. Galutinėje paraiškoje šio klausimo nekils, nes mes iš tikrųjų suderinsime vietas, kuriose mes imamės reljefo, su prašomu detalumo lygiu. Norėdami nuspalvinti savo reljefą, mes tiesiog grąžinsime vertę tarp mėlynos, žalios ir baltos, atsižvelgiant į mėginio taško aukštį.

Tessellating mūsų reljefas

Dabar turime vietovės žemėlapį, apibrėžtą kvadratiniame domene. Turime nuspręsti, kaip tai iš tikrųjų ištrauksime į ekraną. Galėtume apšviesti spindulius į pasaulį ir pabandyti nustatyti, kurią reljefo dalį jie užklumpa, kaip tai darėme ankstesniame straipsnyje. Tačiau šis požiūris būtų labai lėtas. Tai, ką mes padarysime, yra apytiksliai lygus reljefas su daugybe sujungtų trikampių - tai yra, mes išparduosime savo reljefą.

Tessellate: suformuoti arba papuošti mozaika (iš lotynų kalbos) tessellatus).

Norėdami suformuoti trikampio tinklelį, mes tolygiai imsime savo reljefą į įprastą tinklelį ir tada padengsime šį tinklelį trikampiais - po du kiekvienam tinklelio kvadratui. Yra daug įdomių metodų, kuriuos galėtume naudoti supaprastindami šį trikampio tinklelį, tačiau jų mums prireiktų tik tuo atveju, jei rūpėtų greitis.

Šis kodo fragmentas užpildo mūsų reljefo tinklelio elementus fraktalinio reljefo duomenimis. Mes keičiamės žemyn vertikalia savo reljefo ašimi, kad aukštis būtų kiek mažiau perdėtas.

dvigubas perdėjimas =, 7; int lod = 5; int žingsniai = 1 << lod; Trivietis [] žemėlapis = naujas Trivietis [žingsniai + 1] [žingsniai + 1]; Trigubos [] spalvos = naujas RGB [žingsniai + 1] [žingsniai + 1]; Vietovės reljefas = naujas „FractalTerrain“ (lod, .5); už (int i = 0; i <= žingsniai; ++ i) {už (int j = 0; j <= žingsniai; ++ j) {dvigubas x = 1,0 * i / žingsniai, z = 1,0 * j / žingsniai ; dvigubas aukštis = reljefas.getAltitude (x, z); žemėlapis [i] [j] = naujas trigubas (x, aukštis * perdėti, z); spalvos [i] [j] = reljefas.getColor (x, z); }} 

Galbūt klausiate savęs: kodėl trikampiai, o ne kvadratai? Tinklelio kvadratų naudojimo problema yra ta, kad jie nėra lygūs 3D erdvėje. Jei atsižvelgtumėte į keturis atsitiktinius taškus erdvėje, labai mažai tikėtina, kad jie bus lygūs. Taigi vietoj to mes suskaidome savo reljefą į trikampius, nes galime garantuoti, kad bet kokie trys erdvės taškai bus lygūs. Tai reiškia, kad vietovėje nebus jokių spragų, kurias mes nubraižysime.

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