Προγραμματισμός

3D Graphic Java: Δώστε τοπία fractal

Τα τρισδιάστατα γραφικά υπολογιστών έχουν πολλές χρήσεις - από παιχνίδια έως οπτικοποίηση δεδομένων, εικονική πραγματικότητα και άλλα. Τις περισσότερες φορές, η ταχύτητα είναι πρωταρχικής σημασίας, καθιστώντας απαραίτητο το εξειδικευμένο λογισμικό και το υλικό για την ολοκλήρωση της εργασίας. Οι βιβλιοθήκες γραφικών ειδικού σκοπού παρέχουν API υψηλού επιπέδου, αλλά κρύβουν πώς γίνεται η πραγματική εργασία. Ωστόσο, ως προγραμματιστές από τη μύτη προς το μέταλλο, αυτό δεν είναι αρκετά καλό για εμάς! Θα τοποθετήσουμε το API στο ντουλάπι και θα ρίξουμε μια ματιά στο παρασκήνιο για το πώς πραγματικά δημιουργούνται οι εικόνες - από τον ορισμό ενός εικονικού μοντέλου έως την πραγματική απόδοση στην οθόνη.

Θα εξετάσουμε ένα αρκετά συγκεκριμένο θέμα: δημιουργία και απόδοση χαρτών εδάφους, όπως η επιφάνεια του Άρη ή μερικά άτομα χρυσού. Η απόδοση του χάρτη εδάφους μπορεί να χρησιμοποιηθεί για κάτι περισσότερο από αισθητικούς σκοπούς - πολλές τεχνικές οπτικοποίησης δεδομένων παράγουν δεδομένα που μπορούν να αποδοθούν ως χάρτες εδάφους. Οι προθέσεις μου είναι, φυσικά, εντελώς καλλιτεχνικές, όπως μπορείτε να δείτε στην παρακάτω εικόνα! Σε περίπτωση που το επιθυμείτε, ο κώδικας που θα παράγουμε είναι αρκετά γενικός, ώστε με μόνο μικρές τροποποιήσεις μπορεί επίσης να χρησιμοποιηθεί για την απόδοση τρισδιάστατων δομών εκτός από εδάφη.

Κάντε κλικ εδώ για να δείτε και να χειριστείτε το applet εδάφους.

Προετοιμασία για τη συζήτησή μας σήμερα, προτείνω να διαβάσετε τον Ιούνιο "Σχεδίαση σφαιρών υφής" εάν δεν το έχετε κάνει ήδη. Το άρθρο παρουσιάζει μια προσέγγιση ανίχνευσης ακτίνων για την απόδοση εικόνων (πυροβολώντας ακτίνες σε μια εικονική σκηνή για την παραγωγή μιας εικόνας). Σε αυτό το άρθρο, θα εμφανίζουμε στοιχεία σκηνής απευθείας στην οθόνη. Παρόλο που χρησιμοποιούμε δύο διαφορετικές τεχνικές, το πρώτο άρθρο περιέχει κάποιο βασικό υλικό στο java.awt.image πακέτο που δεν θα επαναλάβω σε αυτήν τη συζήτηση.

Χάρτες εδάφους

Ας ξεκινήσουμε ορίζοντας ένα

χάρτης εδάφους

. Ένας χάρτης εδάφους είναι μια λειτουργία που χαρτογραφεί μια 2D συντεταγμένη

(x, ε)

σε υψόμετρο

ένα

και χρώμα

ντο

. Με άλλα λόγια, ένας χάρτης εδάφους είναι απλώς μια συνάρτηση που περιγράφει την τοπογραφία μιας μικρής περιοχής.

Ας ορίσουμε το έδαφος μας ως διεπαφή:

δημόσια διεπαφή Έδαφος {public double getAltitude (double i, double j); δημόσιο RGB getColor (διπλό i, διπλό j); } 

Για τους σκοπούς αυτού του άρθρου θα υποθέσουμε ότι 0,0 <= i, j, υψόμετρο <= 1,0. Αυτό δεν είναι απαίτηση, αλλά θα μας δώσει μια καλή ιδέα πού να βρούμε το έδαφος που θα δούμε.

Το χρώμα του εδάφους μας περιγράφεται απλά ως RGB triplet. Για να παράγουμε πιο ενδιαφέρουσες εικόνες, ίσως εξετάσουμε το ενδεχόμενο προσθήκης άλλων πληροφοριών, όπως η επιφανειακή λάμψη κ.λπ.

δημόσια τάξη RGB {private double r, g, b; δημόσιο RGB (double r, double g, double b) {this.r = r; αυτό.g = g; αυτό.β = β; } δημόσια προσθήκη RGB (RGB rgb) {επιστροφή νέου RGB (r + rgb.r, g + rgb.g, b + rgb.b); } δημόσια αφαίρεση RGB (RGB rgb) {επιστροφή νέου RGB (r - rgb.r, g - rgb.g, b - rgb.b); } δημόσια κλίμακα RGB (διπλή κλίμακα) {επιστροφή νέας RGB (κλίμακα r *, κλίμακα g *, κλίμακα b *) · } ιδιωτικό int toInt (διπλή τιμή) {return (τιμή 1.0); 255: (int) (τιμή * 255.0); } δημόσιο int toRGB () toInt (b); } 

ο RGB Η κλάση ορίζει ένα απλό χρωματικό δοχείο. Παρέχουμε κάποιες βασικές εγκαταστάσεις για την εκτέλεση της αριθμητικής χρώματος και τη μετατροπή ενός χρώματος κυμαινόμενου σημείου σε μορφή συσκευασμένου ακέραιου.

Υπερβατικά εδάφη

Θα ξεκινήσουμε κοιτάζοντας ένα υπερβατικό έδαφος - fancyspeak για ένα έδαφος που υπολογίζεται από ημίτονα και συνημίτονα:

δημόσια τάξη TranscendentalTerrain εφαρμόζει το Έδαφος {ιδιωτικό διπλό άλφα, beta; δημόσιο TranscendentalTerrain (διπλό άλφα, διπλό βήτα) {this.alpha = alpha; this.beta = beta; } public double getAltitude (double i, double j) {return .5 + .5 * Math.sin (i * alpha) * Math.cos (j * beta); } δημόσια RGB getColor (double i, double j) {return new RGB (.5 + .5 * Math.sin (i * alpha), .5 - .5 * Math.cos (j * beta), 0.0); }} 

Ο κατασκευαστής μας δέχεται δύο τιμές που καθορίζουν τη συχνότητα του εδάφους μας. Τα χρησιμοποιούμε για να υπολογίσουμε τα υψόμετρα και τα χρώματα χρησιμοποιώντας Math.sin () και Math.cos (). Θυμηθείτε, αυτές οι συναρτήσεις επιστρέφουν τιμές -1.0 <= sin (), cos () <= 1.0, οπότε πρέπει να προσαρμόσουμε τις τιμές επιστροφής ανάλογα.

Φράκταλ εδάφη

Τα απλά μαθηματικά εδάφη δεν είναι διασκεδαστικά. Αυτό που θέλουμε είναι κάτι που φαίνεται τουλάχιστον αληθινά πραγματικό. Θα μπορούσαμε να χρησιμοποιήσουμε πραγματικά αρχεία τοπογραφίας ως χάρτη εδάφους μας (για παράδειγμα, ο κόλπος του Σαν Φρανσίσκο ή η επιφάνεια του Άρη). Αν και αυτό είναι εύκολο και πρακτικό, είναι κάπως βαρετό. Εννοώ, έχουμε

ήταν

εκεί. Αυτό που πραγματικά θέλουμε είναι κάτι που φαίνεται απίστευτα πραγματικό

και

δεν έχει δει ποτέ πριν. Μπείτε στον κόσμο των fractals.

Ένα φράκταλ είναι κάτι (μια συνάρτηση ή αντικείμενο) που παρουσιάζει αυτο-ομοιότητα. Για παράδειγμα, το σετ Mandelbrot είναι μια συνάρτηση φράκταλ: εάν μεγεθύνετε πολύ το σετ Mandelbrot, θα βρείτε μικροσκοπικές εσωτερικές δομές που μοιάζουν με το ίδιο το κύριο Mandelbrot. Η οροσειρά είναι επίσης φράκταλ, τουλάχιστον σε εμφάνιση. Από κοντά, τα μικρά χαρακτηριστικά ενός μεμονωμένου βουνού μοιάζουν με μεγάλα χαρακτηριστικά της οροσειράς, ακόμη και μέχρι την τραχύτητα μεμονωμένων ογκόλιθων. Θα ακολουθήσουμε αυτήν την αρχή της ομοιότητας για να δημιουργήσουμε τα fractal εδάφη μας.

Ουσιαστικά αυτό που θα κάνουμε είναι να δημιουργήσουμε ένα χονδροειδές, αρχικό τυχαίο έδαφος. Στη συνέχεια, θα προσθέσουμε αναδρομικά επιπλέον τυχαίες λεπτομέρειες που μιμούνται τη δομή του συνόλου, αλλά σε ολοένα και μικρότερες κλίμακες. Ο πραγματικός αλγόριθμος που θα χρησιμοποιήσουμε, ο αλγόριθμος Diamond-Square, περιγράφηκε αρχικά από τους Fournier, Fussell και Carpenter το 1982 (βλ. Πόροι για λεπτομέρειες).

Αυτά είναι τα βήματα που θα εργαστούμε για να φτιάξουμε το fractal έδαφος μας:

  1. Αρχικά εκχωρούμε ένα τυχαίο ύψος στα τέσσερα γωνιακά σημεία ενός πλέγματος.

  2. Στη συνέχεια παίρνουμε τον μέσο όρο αυτών των τεσσάρων γωνιών, προσθέτουμε μια τυχαία διαταραχή και το αντιστοιχίζουμε στο μεσαίο σημείο του πλέγματος (ii στο παρακάτω διάγραμμα). Αυτό ονομάζεται το διαμάντι βήμα γιατί δημιουργούμε ένα μοτίβο διαμαντιών στο πλέγμα. (Στην πρώτη επανάληψη, τα διαμάντια δεν μοιάζουν με διαμάντια επειδή βρίσκονται στην άκρη του πλέγματος. Αλλά αν κοιτάξετε το διάγραμμα θα καταλάβετε τι παίρνω.)

  3. Στη συνέχεια παίρνουμε κάθε ένα από τα διαμάντια που έχουμε δημιουργήσει, κατά μέσο όρο τις τέσσερις γωνίες, προσθέτουμε μια τυχαία διαταραχή και το αντιστοιχίζουμε στο μεσαίο σημείο του διαμαντιού (iii στο παρακάτω διάγραμμα). Αυτό ονομάζεται το τετράγωνο βήμα γιατί δημιουργούμε ένα τετράγωνο μοτίβο στο πλέγμα.

  4. Στη συνέχεια, εφαρμόζουμε ξανά το βήμα διαμαντιών σε κάθε τετράγωνο που δημιουργήσαμε στο τετράγωνο βήμα και μετά εφαρμόστε ξανά το τετράγωνο βήμα προς κάθε διαμάντι που δημιουργήσαμε στο βήμα διαμαντιών, και ούτω καθεξής έως ότου το πλέγμα μας είναι αρκετά πυκνό.

Προκύπτει μια προφανής ερώτηση: Πόσο ενοχλούμε το δίκτυο; Η απάντηση είναι ότι ξεκινάμε με έναν συντελεστή τραχύτητας 0,0 <τραχύτητα <1,0. Κατά την επανάληψη ν του αλγορίθμου Diamond-Square προσθέτουμε μια τυχαία διαταραχή στο πλέγμα: -roughnessn <= διαταραχή <= τραχύτητα. Ουσιαστικά, καθώς προσθέτουμε λεπτομερέστερες λεπτομέρειες στο πλέγμα, μειώνουμε την κλίμακα των αλλαγών που κάνουμε. Οι μικρές αλλαγές σε μικρή κλίμακα είναι κλασικά παρόμοιες με τις μεγάλες αλλαγές σε μεγαλύτερη κλίμακα.

Εάν επιλέξουμε μια μικρή τιμή για τραχύτητα, τότε το έδαφος μας θα είναι πολύ ομαλό - οι αλλαγές θα μειωθούν πολύ γρήγορα στο μηδέν. Εάν επιλέξουμε μια μεγάλη τιμή, τότε το έδαφος θα είναι πολύ τραχύ, καθώς οι αλλαγές παραμένουν σημαντικές σε μικρές διαιρέσεις πλέγματος.

Ακολουθεί ο κώδικας για την εφαρμογή του χάρτη εδάφους fractal:

δημόσια τάξη FractalTerrain εφαρμόζει Έδαφος {ιδιωτικό διπλό [] [] έδαφος; ιδιωτική διπλή τραχύτητα, ελάχιστο, μέγιστο; ιδιωτικά τμήματα int ιδιωτικό τυχαίο rng; δημόσιο FractalTerrain (int lod, διπλή τραχύτητα) {this.roughness = τραχύτητα; this.divisions = 1 << κατάθεση; έδαφος = νέο διπλό [διαιρέσεις + 1] [διαιρέσεις + 1]; rng = νέο τυχαίο (); έδαφος [0] [0] = rnd (); έδαφος [0] [διαιρέσεις] = rnd (); έδαφος [διαιρέσεις] [διαιρέσεις] = rnd (); έδαφος [διαιρέσεις] [0] = rnd (); διπλό τραχύ = τραχύτητα; για (int i = 0; i <lod; ++ i) {int q = 1 << i, r = 1 <> 1; για (int j = 0; j <διαιρέσεις; j + = r) για (int k = 0; k 0) για (int j = 0; j <= διαιρέσεις; j + = s) για (int k = (j + s)% r; k <= διαιρέσεις; k + = r) τετράγωνο (j - s, k - s, r, τραχύ); τραχύ * = τραχύτητα; } min = max = έδαφος [0] [0]; για (int i = 0; i <= διαιρέσεις; ++ i) για (int j = 0; j <= διαιρέσεις; ++ j) εάν (έδαφος [i] [j] max] max = έδαφος [i] [ ι]; } ιδιωτικό κενό διαμάντι (int x, int y, int side, double scale) {if (side> 1) {int half = side / 2; double avg = (έδαφος [x] [y] + έδαφος [x + side] [y] + έδαφος [x + side] [y + side] + έδαφος [x] [y + side]) * 0,25; έδαφος [x + half] [y + half] = avg + rnd () * κλίμακα; }} ιδιωτικό κενό τετράγωνο (int x, int y, int side, double scale) {int half = side / 2; διπλό μέσο = 0,0, άθροισμα = 0,0; εάν (x> = 0) {avg + = έδαφος [x] [y + half]; άθροισμα + = 1.0; } εάν (y> = 0) {avg + = έδαφος [x + half] [y]; άθροισμα + = 1.0; } εάν (x + πλευρά <= διαιρέσεις) {avg + = έδαφος [x + side] [y + half]; άθροισμα + = 1.0; } εάν (y + πλευρά <= διαιρέσεις) {avg + = έδαφος [x + half] [y + side]; άθροισμα + = 1.0; } έδαφος [x + half] [y + half] = μέσος όρος / άθροισμα + rnd () * κλίμακα; } ιδιωτικό διπλό rnd () {return 2. * rng.nextDouble () - 1.0; } δημόσιο διπλό getAltitude (διπλό i, διπλό j) {διπλό alt = έδαφος [(int) (i * διαιρέσεις)] [(int) (j * διαιρέσεις)]; επιστροφή (alt - min) / (max - min); } ιδιωτικό RGB μπλε = νέο RGB (0,0, 0,0, 1,0); ιδιωτικό RGB πράσινο = νέο RGB (0,0, 1,0, 0,0); ιδιωτικό RGB λευκό = νέο RGB (1.0, 1.0, 1.0); δημόσιο RGB getColor (double i, double j) {double a = getAltitude (i, j); εάν (a <.5) επιστροφή blue.add (green.subtract (blue) .scale ((a - 0.0) / 0.5)); αλλιώς επιστρέψτε green.add (white.subtract (green) .scale ((a - 0,5) / 0,5)); }} 

Στον κατασκευαστή, καθορίζουμε και τον συντελεστή τραχύτητας τραχύτητα και το επίπεδο λεπτομέρειας καταθέσει. Το επίπεδο λεπτομέρειας είναι ο αριθμός των επαναλήψεων που πρέπει να εκτελεστούν - για ένα επίπεδο λεπτομέρειας ν, παράγουμε ένα πλέγμα από (2n + 1 x 2n + 1) δείγματα. Για κάθε επανάληψη, εφαρμόζουμε το βήμα διαμαντιού σε κάθε τετράγωνο στο πλέγμα και μετά το τετράγωνο βήμα σε κάθε διαμάντι. Στη συνέχεια, υπολογίζουμε τις ελάχιστες και μέγιστες τιμές δείγματος, τις οποίες θα χρησιμοποιήσουμε για να κλιμακώσουμε τα υψόμετρα εδάφους μας.

Για να υπολογίσουμε το υψόμετρο ενός σημείου, κάνουμε κλίμακα και επιστρέφουμε το πλησιέστερα δείγμα πλέγματος στην ζητούμενη θέση. Στην ιδανική περίπτωση, στην πραγματικότητα θα παρεμβαλλόμαστε μεταξύ των γύρω σημείων δειγματοληψίας, αλλά αυτή η μέθοδος είναι απλούστερη και αρκετά καλή σε αυτό το σημείο. Στην τελική μας αίτηση, αυτό το ζήτημα δεν θα προκύψει επειδή θα αντιστοιχίσουμε πραγματικά τις τοποθεσίες όπου δοκιμάζουμε το έδαφος με το επίπεδο λεπτομέρειας που ζητάμε. Για να χρωματίσουμε το έδαφος μας, επιστρέφουμε απλώς μια τιμή μεταξύ μπλε, πράσινου και λευκού, ανάλογα με το υψόμετρο του δείγματος.

Tessellating το έδαφος μας

Έχουμε τώρα έναν χάρτη εδάφους που ορίζεται σε έναν τετραγωνικό τομέα. Πρέπει να αποφασίσουμε πώς θα το σχεδιάσουμε πραγματικά στην οθόνη. Θα μπορούσαμε να πυροδοτήσουμε τις ακτίνες στον κόσμο και να προσπαθήσουμε να προσδιορίσουμε ποιο τμήμα του εδάφους χτυπούν, όπως κάναμε στο προηγούμενο άρθρο. Ωστόσο, αυτή η προσέγγιση θα ήταν εξαιρετικά αργή. Αυτό που θα κάνουμε αντ 'αυτού είναι η προσέγγιση του ομαλού εδάφους με μια δέσμη συνδεδεμένων τριγώνων - δηλαδή, θα περιορίσουμε το έδαφος μας.

Tessellate: για να σχηματιστεί ή να στολιστεί με μωσαϊκό (από τα λατινικά tessellatus).

Για να σχηματίσουμε το πλέγμα τριγώνου, θα δοκιμάσουμε ομοιόμορφα το έδαφος μας σε ένα κανονικό πλέγμα και στη συνέχεια θα καλύψουμε αυτό το πλέγμα με τρίγωνα - δύο για κάθε τετράγωνο του πλέγματος. Υπάρχουν πολλές ενδιαφέρουσες τεχνικές που θα μπορούσαμε να χρησιμοποιήσουμε για να απλοποιήσουμε αυτό το τρίγωνο πλέγμα, αλλά θα χρειαζόμασταν αυτές μόνο εάν η ταχύτητα ήταν ανησυχητική.

Το ακόλουθο τμήμα κώδικα συμπληρώνει τα στοιχεία του πλέγματος εδάφους μας με δεδομένα εδάφους φράκταλ. Μειώνουμε τον κατακόρυφο άξονα του εδάφους μας για να κάνουμε τα υψόμετρα λίγο λιγότερο υπερβολικά.

διπλή υπερβολή = .7; int lod = 5; int βήματα = 1 << κατάθεση; Triple [] map = new Triple [βήματα + 1] [βήματα + 1]; Τριπλή [] χρώματα = νέο RGB [βήματα + 1] [βήματα + 1]; Έδαφος εδάφους = νέο FractalTerrain (lod, .5); για (int i = 0; i <= βήματα; ++ i) {για (int j = 0; j <= βήματα; ++ j) {διπλά x = 1,0 * i / βήματα, z = 1,0 * j / βήματα ; διπλό υψόμετρο = terrain.getAltitude (x, z); map [i] [j] = νέο Τριπλό (x, υψόμετρο * υπερβολή, z); χρώματα [i] [j] = terrain.getColor (x, z); }} 

Μπορεί να αναρωτιέστε: Γιατί λοιπόν τρίγωνα και όχι τετράγωνα; Το πρόβλημα με τη χρήση των τετραγώνων του πλέγματος είναι ότι δεν είναι επίπεδα σε 3D χώρο. Εάν λάβετε υπόψη τέσσερα τυχαία σημεία στο διάστημα, είναι πολύ απίθανο να είναι συμπαγή. Αντ 'αυτού, αποσυνθέτουμε το έδαφος μας σε τρίγωνα, διότι μπορούμε να εγγυηθούμε ότι οποιαδήποτε τρία σημεία στο διάστημα θα είναι συμπαγή. Αυτό σημαίνει ότι δεν θα υπάρχουν κενά στο έδαφος που καταλήγουμε να σχεδιάζουμε.