OpenGL: Μάθημα 4ο - 3D Σχήματα
Ο κώδικας που ακολουθεί έχει ληφθεί από την ιστοσελίδα
https://www3.ntu.edu.sg/home/ehchua/programming/opengl/CG_Examples.html.
Έχουν προστεθεί σχόλια για καλύτερη κατανόηση. Τα σχόλια εστιάζουν σε τμήματα κώδικα που δεν έχουν αναλυθεί σε βάθος σε προηγούμενα μαθήματα ή τα συναντούμε για πρώτη φορά εδώ. Συγκεκριμένα γίνεται εκτενής αναφορά στις παρακάτω συναρτήσεις και έννοιες:
? glutDisplayFunc
? glutInitDisplayMode
? glClearColor
? glutMainLoop()
? glClearDepth
? glClear
? glEnable(GL_DEPTH_TEST) και glDepthFunc
? glBegin(GL_QUADS)
? local space ή model space
? world space
? view space
? viewport transform
? projection transform
1 |
/* |
Επανασχεδιασμός του παραθύρου
glutDisplayFunc(display); // Register callback handler for window re-paint event
Η εντολή glutDisplayFunc(display) δηλώνει τη συνάρτηση display() σαν χειριστή του συμβάντος re-paint (επανασχεδιασμός). Δηλαδή, το υποσύστημα γραφικών πυροδοτεί την display() όταν το παράθυρο πρωτοεμφανίζεται και οποτεδήποτε υπάρχει ένα re-paint αίτημα (συμβάν επανασχεδιασμού). Πότε όμως υπάρχει re-paint αίτημα, δηλαδή πότε πυροδοτείται η display callback function; H GLUT είναι αυτή που καθορίζει πότε η συνάρτηση display() πρέπει να πυροδοτηθεί, βασισμένη στην κατάσταση επανασχεδιασμού του παραθύρου (window's redisplay state). Η redisplay state για ένα παράθυρο μπορεί να τεθεί ευθέως με κλήση της glutPostRedisplay ή εμμέσως σαν αποτέλεσμα ενός window damage το οποίο αναφέρεται από το window system (π.χ αλλαγή μεγέθους παραθύρου).
Αξίζει να σημειωθεί το εξής "παράδοξο". Όταν δημιουργείται ένα παράθυρο, δεν υπάρχει αυτόματα κλήση κάποιας συνάρτησης εμφάνισης του παραθύρου. Είναι ευθύνη του προγραμματιστή να ορίσει μια συνάρτηση πυροδοτούμενη από το γεγονός της δημιουργίας του παραθύρου. Συγκεκριμένα η δημιουργία του παραθύρου ενεργοποιεί τη redisplay state του παραθύρου, η οποία με τη σειρά της προκαλεί re-paint event. Αν δεν έχει δηλωθεί συνάρτηση χειριστής του re-paint συμβάντος τότε προκύπτει "μοιραίο λάθος" (fatal error).
Βασικές παράμετροι εμφάνισης του παραθύρου
glutInitDisplayMode(GLUT_DOUBLE); // Enable double buffered mode
Η glutInitDisplayMode θέτει τον αρχικό τρόπο εμφάνισης του προς δημιουργία παραθύρου. Τα ορίσματά της χωρίζονται με τον τελεστή | (OR). Παράδειγμα χρήσης της είναι η glutInitDisplayMode(GLUT_DOUBLE| GLUT_RGBA). Ορίζει ότι το παράθυρο που θα δημιουργηθεί θα είναι double buffered με RGBA χρωματικό κώδικα. Στην περίπτωση του κώδικα που εξετάζουμε απουσιάζει το όρισμα GLUT_RGBA αν και σε επόμενη εντολή ( την glClearColor(0.0f, 0.0f, 0.0f, 1.0f)) ορίζεται RGB χρώμα (το το 0.0f, 0.0f, 0.0f) και διαφάνεια Α (η 1.0f). Αυτό συμβαίνει διότι το GLUT_RGBA είναι η προκαθορισμένη τιμή αν ούτε η GLUT_RGBA ούτε η εναλλακτική της GLUT_INDEX ορίζονται ρητώς (το ίδιο ισχύει και για άλλα αμοιβαία αποκλειόμενα ορίσματα - glutInitDisplayMode. Είναι προτιμότερο να γίνεται χρήση της σαφούς εντολής glutInitDisplayMode(GLUT_DOUBLE| GLUT_RGBA).
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Set background color to black and opaque
Γίνεται επιλογή χρώματος όχι χρωματισμός. Ορίζονται δηλαδή οι τιμές red, green, blue και alpha (διαφάνεια) οι οποίες θα χρησιμοποιηθούν από την glClear για την αρχικοποίηση των συσσωρευτών χρωμάτων (color buffers).
glutMainLoop
glutMainLoop(); // Enter the infinite event-processing loop
Για να γίνει καλύτερα κατανοητός ο τρόπος λειτουργίας της glutMainLoop() τον αναπαριστούμε με ψευδοκώδικα.
glutMainLoop()
{
ΕΠΑΝΑΛΑΒΕ
{
έλεγξε για συμβάντα (events)
ΑΝ δεν υπάρχουν συμβάντα ΤΟΤΕ
ΑΝ υπάρχει συνάρτηση χρήστη για χειρισμό του idle event ΤΟΤΕ κάλεσε την idle συνάρτηση χρήστη
ΑΛΛΙΩΣ
ΠΕΡΙΠΤΩΣΗ συμβάν
1 {ΑΝ υπάρχει συνάρτηση χειρισμού για το συμβάν 1 ΤΟΤΕ κάλεσε τη συνάρτηση χειρισμού}
2 {ΑΝ υπάρχει συνάρτηση χειρισμού για το συμβάν 2 ΤΟΤΕ κάλεσε τη συνάρτηση χειρισμού}
3 {ΑΝ υπάρχει συνάρτηση χειρισμού για το συμβάν 3 ΤΟΤΕ κάλεσε τη συνάρτηση χειρισμού}
.
.
.
ν {ΑΝ υπάρχει συνάρτηση χειρισμού για το συμβάν ν ΤΟΤΕ κάλεσε τη συνάρτηση χειρισμού}
}
}
DepthBuffer & ColorBuffer
glClearDepth(1.0f); // Set background depth to farthest
Η εντολή glClearDepth επιλέγει την τιμή που θα εγγραφεί στον Depth Buffer όταν κληθεί η glClear. Δείτε το ρόλο του Depth Buffer στη διαδικασία χρωματισμού των pixels.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear color and depth buffers
Η glClear χρωματίζει την ενεργή περιοχή του παραθύρου σύμφωνα με τις τιμές (χρώματα & τοποθέτηση στον άξονα Ζ) που έχουν ήδη οριστεί από τις glClearColor και glClearDepth. Η εντολή glClear τοποθετείται στην αρχή της συνάρτησης diplay(), η οποία χειρίζεται τα re-paint συμβάντα. Με άλλα λόγια πρόκειται για εντολή αρχικοποίησης και καλείται πριν παρασταθούν τα γραφικά στο παράθυρο. Προκαλεί αρχικοποίηση
α. του χρώματος παρασκηνίου (background) και της διαφάνειας του παρασκηνίου (στην ενεργή περιοχή του παραθύρου)
β. του βάθους του παρασκηνίου (στην ενεργή περιοχή του παραθύρου)
Δείτε στο επόμενο βίντεο (από 5:24'') τι συμβαίνει αν ξεχάσουμε να καθαρίσουμε τον Color Buffer καθώς και τη διαδικασία εναλλαγής Buffer Swap.
glEnable(GL_DEPTH_TEST); // Enable depth testing for z-culling
glDepthFunc(GL_LEQUAL); // Set the type of depth-test
Η σύγκριση που υποδηλώνεται στην εντολή glDepthFunc(GL_LEQUAL) γίνεται μόνο αν έχει προηγηθεί η ενεργοποίηση του ελέγχου βάθους στον άξονα z (με την εντολή glEnable(GL_DEPTH_TEST))
Η εντολή glDepthFunc ορίζει τη συνάρτηση "τελεστή σύγκρισης" η οποία χρησιμοποιείται για να συγκρίνει το βάθος κάθε νεοεισερχόμενου pixel με το βάθος που υπάρχει στον Depth Buffer (για το εν λόγω pixel).
Με GL_EQUAL το νεοεισερχόμενο pixel αναπαρίσταται στην οθόνη μόνο αν το βάθος του είναι ίσο με την υπάρχουσα στον Depth Buffer τιμή βάθους. Άλλες πιθανές τιμές ορίσματος είναι οι
GL_NEVER, GL_LESS, GL_EQUAL, GL_LEQUAL, GL_GREATER,GL_NOTEQUAL, GL_GEQUAL, και GL_ALWAYS. Προκαθοριμένη τιμή είναι η GL_LESS
Για να γίνει πιο κατανοητή η διαδικασία παρακολουθήστε το βίντεο του Jamie King, το οποίο δείχνει πως να χειριστούμε το depth test (z test) με χρήση της glEnable(GL_DEPTH_TEST) προκειμένου να επιτρέψουμε την εγγραφή στον depth buffer (z-buffer). Πρέπει να βάλουμε σε τάξη (να ταξινομήσουμε) τη γεωμετρία μας στον άξονα των Z. Ομοίως, στο βίντεο αποσαφηνίζεται το πώς καθαρίζεται ο depth buffer με χρήση της glClear(GL_DEPTH_BUFFER_BIT) και πως αυτές οι λειτουργίες επηρεάζουν αυτό που τελικά αναπαρίσταται στην οθόνη.
Ορισμός 3D αντικειμένων
glBegin(GL_QUADS); // Begin drawing the color cube with 6 quads // Top face (y = 1.0f) // Define vertices in counter-clockwise (CCW) order with normal pointing out glColor3f(0.0f, 1.0f, 0.0f); // Green glVertex3f( 1.0f, 1.0f, -1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
Κάθε αντικείμενο της OpenGL συγκροτείται από οντότητες (primitives) όπως τρίγωνα, τετράγωνα, πολύγωνα, σημεία και γραμμές. Μια οντότητα ορίζεται από μία ή περισσότερες κορυφές (vertices). Ο έγχρωμος κύβος αποτελείται από 6 τετράγωνα. Κάθε τετράγωνο αποτελείται από 4 κορυφές. Στον κώδικα που προηγείται και οι 4 κορυφές του τετραγώνου (ενός εκ των 6 τετραγώνων) έχουν πράσινο χρώμα (0.0f, 1.0f, 0.0f).
Ο έγχρωμος κύβος (δηλαδή τα 6 τετράγωνα) ορίζεται στο τοπικό ορθοκανονικό σύστημα αξόνων (local space ή model space) με αρχή των αξόνων x, y, z το κέντρο του κύβου (στο εσωτερικό του 3D σχήματος).
Ο άξονας x έχει φορά (θετικές τιμές) προς τα δεξιά, ο άξονας y κατακόρυφα προς τα πάνω και ο άξονας z φορά προς τα έξω.
Το μήκος της κάθε πλευράς του τετραγώνου (και των υπολοίπων 5 τετραγώνων) είναι 2 μονάδες.
Είναι εύκολο να αντιληφθούμε πλέον ότι οι επόμενες εντολές ορίζουν τις κορυφές V6, V5, V1 και V4.
glVertex3f( 1.0f, 1.0f, -1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
Με άλλα λόγια ορίζουν την πάνω έδρα του κύβου.
Με όμοιο τρόπο ορίζονται στο τοπικό (για το κάθε αντικείμενο) σύστημα συντεταγμένων (local space ή model space) οι υπόλοιπες κορυφές του κύβου και της πυραμίδας. Οι κορυφές είναι διανύσματα (π.χ. η κορυφή V3 είναι διάνυσμα με αρχή το σημείο τομής των αξόνων του local space 0,0,0 και τέλος το σημείο 1,-1,1 με φορά από το 0,0,0 προς το 1,-1,1).
Τα local spaces (ή model spaces) είναι όσα και τα αντικείμενα που ορίζονται στο OpenGL πρόγραμμα.
MODEL TO WORLD (Model Transform)
Αφού τα αντικείμενα δημιουργηθούν στο δικό τους τοπικό χώρο (local space) πρέπει να μεταφερθούν στον κοινό για όλα τα αντικείμενα χώρο, δηλαδή στο World Space.
Παρατηρήστε ότι τα κέντρα των επιμέρους local spaces έχουν μετασχηματιστεί, διότι έχουν πλέον συντεταγμένες (? 0,0,0) ως προς την αρχή του συστήματος συντεταγμένων World Space. Κατά ανάλογο τρόπο όλες οι κορυφές όλων των αντικειμένων που μεταφέρονται στο World Space μετασχηματίζονται διότι λαμβάνουν συντεταγμένες ως προς το κέντρο του. Οι κορυφές είναι πάντα διανύσματα με αρχή το σημείο 0,0,0 του κοινού World Space (και όχι του δικού τους Local Space)
Η διαδικασία που περιγράφηκε ονομάζεται Model Transformation ή Model to World Transformation (μετασχηματισμός) και υλοποιείται με τις εντολές που ακολουθούν.
glMatrixMode(GL_MODELVIEW); // To operate on model-view matrix // Render a color-cube consisting of 6 quads with different colors glLoadIdentity(); // Reset the model-view matrix
Η εντολή glMatrixMode καθορίζει σε ποιον πίνακα θα συμβούν οι επερχόμενες πράξεις πινάκων. Συγκεκριμένα, η glMatrixMode(GL_MODELVIEW) ορίζει ότι θα χρησιμοποιηθεί ο πίνακας MODELVIEW ο οποίος (από κατασκευής) προορίζεται να υποστεί μεταβολές (πράξεις) που αντιπροσωπεύουν τους μετασχηματισμούς των αντικειμένων στον κοινό κόσμο (World Space). Τέτοιοι μετασχηματισμοί είναι η κλιμάκωση (scaling / εντολή glScale), περιστροφή (rotation / εντολή glRotate) και μετατόπιση (translation / εντολή glTranslate) ενός αντικειμένου. Η εντολή glMatrixMode(GL_MODELVIEW) καλείται μόνο μια φορά. Συχνά ο πίνακας MODELVIEW αναφέρεται και σαν MODEL TO WORLD πίνακας (το όνομα MODEL TO WORLD παραπέμπει στη χρήση του πίνακα).
Ακολουθεί η γενική μορφή του πίνακα MODELVIEW (χωρίς λεπτομέρειες).
Η εντολή glLoadIdentity() καλείται κάθε φορά που σκοπεύουμε να ασκήσουμε μετασχηματισμούς σε διαφορετικό αντικείμενο, προκειμένου να αρχικοποιήσει τον πίνακα MODELVIEW. Αυτό συμβαίνει για τον εξής λόγο: Οι εντολές glTranslate και glRotate λειτουργούν πάντα σχετικά ως προς την τρέχουσα κατάσταση του MODELVIEW πίνακα. Έτσι, αν καλέσουμε τη glTranslate μετατοπίζουμε από την "τρέχουσα θέση" του πίνακα MODELVIEW, όχι από την αρχική (προκαθορισμένη) θέση. Αν επιθυμούμε να μετατοπίσουμε με σημείο αναφοράς την αρχική θέση του πίνακα πρέπει να καλέσουμε τη glLoadIdentity(), η οποία επανατοποθετεί τον πίνακα MODELVIEW στην αρχική του θέση. Ομοίως, μπορούμε να περιστρέψουμε (glRotate) με πίνακα αναφοράς τον MODELVIEW, ο οποίος είναι τώρα προσανατολισμένος στην αρχική κατεύθυνση.
Ο MODEL to WORLD (MODELVIEW) πίνακας διατηρείται για κάθε αντικείμενο που εισέρχεται στο World space, ανεξάρτητα από το αν η εντολή glLoadIdentity() θα τον αρχικοποιήσει για άσκηση μετασχηματισμού σε άλλο αντικείμενο. Κάθε αντικείμενο του World space κουβαλάει το δικό του MODELVIEW πίνακα.
Με βάση τα παραπάνω είναι κατανοητή η χρήση των εντολών
glMatrixMode(GL_MODELVIEW); // To operate on model-view matrix // Render a color-cube consisting of 6 quads with different colors glLoadIdentity(); // Reset the model-view matrix glTranslatef(1.5f, 0.0f, -7.0f); // Move right and into the screen
Σημειώνεται ότι οι εντολές σχεδιασμού των αντικειμένων έπονται των εντολών MODEL TO WORLD. Φανταστείτε ότι πρώτα τοποθετούμε στο World space το κέντρο του αντικειμένου και ακολούθως σχεδιάζουμε (ως προς αυτό το κέντρο) το ίδιο το αντικείμενο (local space συντεταγμένες).
// Render a pyramid consists of 4 triangles glLoadIdentity(); // Reset the model-view matrix glTranslatef(-1.5f, 0.0f, -6.0f); // Move left and into the screen glBegin(GL_TRIANGLES); // Begin drawing the pyramid with 4 triangles // Front glColor3f(1.0f, 0.0f, 0.0f); // Red glVertex3f( 0.0f, 1.0f, 0.0f); glColor3f(0.0f, 1.0f, 0.0f); // Green glVertex3f(-1.0f, -1.0f, 1.0f); glColor3f(0.0f, 0.0f, 1.0f); // Blue glVertex3f(1.0f, -1.0f, 1.0f);
WORLD TO VIEW (View Transform)
Αφού μετασχηματιστούν τα αντικείμενα (μετατόπιση, κλιμάκωση, περιστροφή στο WORLD space) θα περίμενε κανείς ότι αυτά γίνονται αυτόματα ορατά. Κάτι τέτοιο δε συμβαίνει.
Πρέπει τα αντικείμενα να εισέλθουν στο οπτικό πεδίο του παρατηρητή (camera).
Η προκαθορισμένη θέση της κάμερας (με world space συντεταγμένες) είναι:
gluLookAt(0.0, 0.0, 0.0, 0.0, 0.0, -100.0, 0.0, 1.0, 0.0)
Το μάτι (camera) EYE=(0,0,0) LookAT=(0,0,-100) κοιτάζει μέσα (στα αρνητικά του άξονα z προς το εσωτερικό της οθόνης), και UP=(0,1,0) πάνω (στην κατεύθυνση του άξονα y).
Στο σημείο EYE τοποθετούνται τα μάτια σου. Τα σημεία EYE και LookΑΤ ορίζουν τον άξονα θέασης (προς τα που βλέπουν τα ευθυτενή μάτια σου). Ο άξονας (διάνυσμα) αυτός ορίζεται από την κίνηση του κεφαλιού σου. Το UP ορίζει το πόσο ψηλά η χαμηλά κοιτούν τα μάτια σου (θεωρούμε ότι τα μάτια δε στρίβουν αλλά η κίνηση αυτή γίνεται μόνο από το κεφάλι).
Το OpenGL graphics rendering pipeline (το HW δηλαδή) πραγματοποιεί WORLD TO VIEW μετασχηματισμό προκειμένου να φέρει το world space στο view space της κάμερας (στην περίπτωση της προκαθορισμένης θέσης της κάμερας δε χρειάζεται μετασχηματισμός). Ο όρος WORLD TO VIEW μετασχηματισμός σημαίνει ότι υπάρχει μοναδικός πίνακας WORLD TO VIEW, τον οποίο μοιράζονται οι MODEL to WORLD πίνακες των επιμέρους αντικειμένων. Καθώς ο κόσμος (World) περιστρέφεται γύρω από το σημείο όπου εδράζεται η κάμερα υφίσταται πράξεις (αλλαγές) ο πίνακας WORLD TO VIEW.
Βίντεο για κατανόηση μετασχηματισμών MODEL TO WORLD και WORLD TO VIEW
Πως αλλάζουν οι Πίνακες κατά τους μετασχηματισμούς; Απάντηση στο ερώτημα δίνει το επόμενο βίντεο του Jamie King.
Ο Jamie King περιγράφει με ενάργεια τους μετασχηματισμούς Model to World και World to View (Camera) εστιάζοντας στα 3 εμπλεκόμενα συστήματα συντεταγμένων (τα οποία μπορούν αν ειδωθούν και σαν ένα ενιαίο σύστημα συντεταγμένων).
Viewport Transform
Αφορά τον επανασχεδιασμό των αντικειμένων στην ωφέλιμη περιοχή του παραθύρου με τρόπο που η δυσανάλογη αλλαγή διαστάσεων του παραθύρου να μην προκαλεί παραμόρφωση στα αντικείμενα. Πραγματοποιείται με τις επόμενες εντολές:
// Set the viewport to cover the new window
glViewport(0, 0, width, height);
Για λεπτομέρειες ανατρέξτε στο "Μάθημα 2ο - Μετασχηματισμός παραθύρου"
VIEW TO PROJECTION (Projection Transform)
Στον WORLD TO VIEW μετασχηματισμό ο κόσμος της κάμερας είναι τρισδιάστατος. Για να αναπαρασταθεί η σκηνή στην οθόνη πρέπει η τρισδιάστατη απεικόνιση να "στριμωχτεί" στην οθόνη ώστε να χάσει την 3η διάσταση. Αυτό ακριβώς κάνει η προβολή (projection).
Μια κάμερα έχει περιορισμένο οπτικό πεδίο. Η προβολή (projection) μοντελοποιεί την άποψη που συλλαμβάνει η κάμερα. Υπάρχουν δύο είδη προβολής:
Προοπτική προβολή (perspective projection) και ορθογραφική προβολή (orthographic projection). Στην perspective projection, τα απομακρυσμένα από την κάμερα αντικείμενα φαίνονται μικρότερα συγκρινόμενα με ίδιου μεγέθους αντικείμενα, τα οποία βρίσκονται πιο κοντά στην κάμερα.
Στην orthographic projection, τα αντικείμενα φαίνονται ισομεγέθη (όταν είναι και από κατασκευής ισομεγέθη) ανεξάρτητα από το βάθος (τιμή στον άξονα z). Η Orthographic projection είναι μια ειδική περίπτωση perspective projection κατά την οποία η κάμερα είναι τοποθετημένη πολύ μακριά από τα αντικείμενα.
Δείτε εδώ το σχεδιαστικό κομμάτι του projection με χρήση κανόνων γεωμετρίας προκειμένου να γίνει περισσότερο κατανοητό τι σημαίνει projection (προβολή). Ομοίως δείτε εδώ μια προσομοίωση perspective vs orthographic projection.
Για να ορίσουμε την προβολή πρέπει να δουλέψουμε στον πίνακα Projection (ή αλλιώς VIEW TO PROJECTION πίνακα), με χρήση της εντολής
glMatrixMode(GL_PROJECTION); // To operate on the Projection matrix
ενώ δεν ξεχνάμε να αρχικοποιήσουμε με χρήση της
glLoadIdentity(); // Reset
Χρησιμοποιούμε την εντολή gluPerspective() για να ενεργοποιήσουμε την perspective projection.
// Enable perspective projection with fovy, aspect, zNear and zFar
gluPerspective(45.0f, aspect, 0.1f, 100.0f);
όπου fovy = 45ο η γωνία θέασης από την κάτω στην πάνω έδρα της κόλουρης πυραμίδας, aspect ο λόγος width/height, zNear και zFar τα εγγύς και μακράν επίπεδα της κόλουρης πυραμίδας.
Παρατηρήστε ότι κατά τον μετασχηματισμό MODEL TO WORLD η μετατόπιση
glTranslatef(1.5f, 0.0f, -7.0f); // Move right and into the screen
και
glTranslatef(-1.5f, 0.0f, -6.0f); // Move left and into the screen
οδηγεί τα κέντρα των αντικειμένων εντός της κόλουρης πυραμίδας θέασης.
Ο μετασχηματισμός VIEW TO PROJECTION μετασχηματίζει την κόλουρη πυραμίδα θέασης σε ορθογώνιο παραλληλεπίπεδο 2x2x1 που λειτουργεί ως απόκομμα όγκου (clipping-volume) κεντραρισμένο στο εγγύς επίπεδο. Ο επακόλουθος Viewport μετασχηματισμός μετασχηματίζει το clipping-volume σε περιοχή θέασης της δισδιάστατης οθόνης (screen space).
Ασκήσεις εμπέδωσης
Πατήστε εδώ για "Ασκήσεις 4ου μαθήματος: 3D Shapes"
Hits: 3466