Εισαγωγή στον προγραμματισμό 3D γραφικών με το OpenGL

Γιάννης Τσιομπίκας

Αυτή η σειρά άρθρων αποσκοπεί στην γνωριμία τον αναγνωστών με τον προγραμματισμό 3D γραφικών, και πιο συγκεκριμένα με τα realtime 3D γραφικά, χρησιμοποιώντας το OpenGL API.

Όταν μιλάμε για 3D γραφικά, αναφερόμαστε σε διάφορους αλγορίθμους που χρησιμοποιούμε, ώστε απο μια μαθηματική περιγραφή ενος τρισδιάστατου περιβάλλωντος, να δημιουργήσουμε μια εικόνα που να αναπαριστά αυτό το 3D περιβάλλων στην οθόνη του υπολογιστή. Λέγοντας οτι θα ασχολιθούμε συγκεκριμένα με realtime γραφικά, ενοούμε οτι μας ενδιαφέρει αυτές οι εικόνες να υπολογίζονται σε κλάσματα του δευτερολέπτου ώστε να μπορούμε να αλληλεπιδράμε με τον 3D κόσμο, και να βλέπουμε άμεσα τις αλλαγές αυτες στην οθόνη μας. Κλασικό παράδειγμα χρήσης realtime 3D γραφικών είναι τα computer games, όπου τα γραφικά ανανεώνονται συνεχώς, για να δώσουν την αίσθηση τις κίνισης στον παίχτη.

Το πρώτο πρόβλημα που καλούμαστε να αντιμετοπίσουμε, πριν περάσουμε στους αλγορίθμους rendering, είναι πως θα αναπαραστίσουμε μια 3D σκηνή στο πρόγραμμα μας. Ο πιο συνηθισμένος τρόπος είναι να χρησιμοποιήσουμε μια σειρά απο πολύγωνα, που προσεγγίζουν την επιφάνεια του κάθε αντικειμένου (βλ. σχήμα 1). Αυτό το representation μας παρέχει απέραντη ευελιξία να αναπαραστίσουμε οποιαδήποτε 3D επιφάνεια θέλουμε, σε ότι βαθμό προσσέγγισης θέλουμε.

polygonal mesh approximation

Σχήμα 1: Πολυγωνική προσέγγιση αντικειμένων

Υπάρχουν δύο βασικές τεχνικές για το rendering 3D γραφικών. Η πιό απλή και elegant μέθοδος rendering, λέγεται ray-tracing. Το πρόγραμμα "ρίχνει" ακτίνες για κάθε pixel, και υπολογίζει σημεία τομής αυτών τον ακτίνων με τα διάφορα αντικείμενα της σκηνής. Αν και τα αποτελέσματα του ray-tracing μπορούν να είναι πολύ ρεαλιστικά, είναι αρκετά χρονοβόρα διαδικασία, και για αυτο τον λόγο δεν χρησιμοποιείται στα realtime γραφικά. Η δεύτερη τεχνική βασίζεται στο polygon rasterization. Ουσιαστικά προβάλλουμε τα 3D πολύγωνα των αντικειμένων στο 2D επίπεδο της εικόνας, και μετά χρωματίζουμε τα pixels που περιέχονται στο κάθε πολύγωνο με το κατάλληλο χρώμα. Με αυτή την τελευταία τεχνική θα ασχοληθούμε, γιατί είναι αρκετά γρήγορη ώστε να χρησιμεύει στα realtime προγράμματα γραφικών, και επίσης είναι υλοποιημένη κατα μεγάλο μέρος στο specialized graphics hardware που έχουμε εν αυθονία στους υπολογιστές μας τα τελευταία χρόνια.

Το OpenGL, είναι ενα API για το rendering 3D γραφικών με την μέθοδο του polygon rasterization. Φτιάχτικε απο την Silicon Graphics (SGI) στις αρχές του 90, ως μια απόπειρα να δημιουργιθεί ένα standard interface για τις rendering δυνατότητες των διαφόρων graphics workstations. Παρόλο που ξεκίνησε απο την SGI, το OpenGL αναπτύχθηκε ως ενα vendor-independent standard, το οποίο κατευθύνεται απο ενα committee, αποτελούμενο απο διάφορες εταιρίες και οργανισμούς που ασχολούντε με τον χώρο, το OpenGL Architecture Review Board (ARB).

Αντίθετα με τον προηγούμενο αντίστοιχο API της Silicon Graphics, το IrisGL, το οποίο ήταν άρρικτα συνδεδεμέμο με το X window system, το OpenGL είναι window-system agnostic. Δέν ασχολέιτε με window creation, event handling (input), κλπ, αλλα επαφίεται σε κάποιο ξεχωριστό window system specific glue layer (GLX) για να συνδέσει το OpenGL rendering context με κάποιο παράθυρο. Παρ'όλα αυτά, σε αυτά τα άρθρα δέν θα ασχοληθούμε με το GLX και το X Window System απευθείας, μιας και κάτι τέτοιο θα ήθελε δικό του ξεχωριστό άρθρο, αλλα θα χρησιμοποιήσουμε μια απλή και εύχριστη cross-platform βιβλιοθήκη ονόματι GLUT (GL Utility Toolkit), που αναλαμβάνει να μας ανοίξει OpenGL παράθυρα, και παρέχει ενα απλο callback interface για event handling.

Ορίστε ένα παράδειγμα χρήσης της βιβλιοθήκης GLUT, το οποίο ανοίγει ενα παράθυρο 800x600, και χειρίζεται κάποια events. Επίσης σε αυτό, καλούμε και μια συνάρτηση του OpenGL για να καθαρίσουμε την εικόνα γεμίζοντας την με μαύρο χρώμα. (βλ. εικόνα 1) Αυτό θα πρέπει να το κάνουμε κάθε φορά που ανανεώνουμε την εικόνα, αλλιώς θα βλέπουμε σκουπίδια απο τα προηγούμενα καρέ, οπότε το τοποθετούμε στην αρχή του display callback, το οποίο καλείται αυτόματα απο το GLUT κάθε φορα που χρειάζεται να ανανεωθεί η εικόνα στο παράθυρο μας. Επίσης θα παρατιρήσετε οτι στο τέλος της display συνάρτησης, καλούμε και την συνάρτηση glutSwapBuffers. Αυτό το call είναι απαραίτητο για να εμφανιστούν τα όσα ζωγραφίσαμε στην οθόνη, λόγο του ότι χρησιμοποιούμε double buffering, το οποίο σημαίνει οτι ζωγραφίζουμε σε ενα μή ορατό κομμάτι μνήμης, και μόνο αφού τελειώσουμε το εμφανίζουμε στην οθόνη.

Για να κάνουμε compile το παρακάτω πρόγραμμα, το οποίο ας πούμε οτι το έχουμε σε ενα αρχείο gl1.c, καλούμε τον C compiler του συστήματος ως εξής: cc -o gl1 gl1.c -lGL -lglut

#include <stdio.h>
#include <ctype.h>
#include <GL/glut.h>  /* glut.h also includes <GL/gl.h> (the OpenGL header) */

void display(void);
void reshape(int x, int y);
void keyb(unsigned char key, int x, int y);

int main(int argc, char **argv)
{
    /* initialize glut, and create a window */
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE);
    glutInitWindowSize(800, 600);
    glutCreateWindow("OpenGL window");

    /* register event callback handlers */
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutKeyboardFunc(keyb);

    /* enter the glut main event handling loop, this never returns */
    glutMainLoop();
    return 0;
}

/* the display callback is called when we need to redraw the graphics in our
 * OpenGL window (due to X11 Expose event, or call to glutPostRedisplay).
 */
void display(void)
{
    /* clear the image (by default clears to black) */
    glClear(GL_COLOR_BUFFER_BIT);

    /* since we requested a double-buffered visual (GLUT_DOUBLE flag at
     * glutInit), we need to "show" the back buffer by calling glutSwapBuffers.
     */
    glutSwapBuffers();
}

/* the reshape callback is called when our window is resized */
void reshape(int x, int y)
{
    printf("window resized: %dx%d\n", x, y);
}

/* the keyboard callback is called when a key is pressed */
void keyb(unsigned char key, int x, int y)
{
    if(isprint(key)) {
        printf("pressed: '%c'\n", key);
    } else {
        printf("pressed: 0x%x\n", key);
    }
}

Πρίν προχωρίσουμε παραπέρα πρέπει να πούμε λίγα πράγματα για το πώς τα πολύγωνα τον αντικειμένων φτάνουν να ζωγραφιστούν στην οθόνη. Οι κορυφές (vertices), που ορίζουν το κάθε πολύγωνο (τα οποία ειναι φυσικά 3-διάστατα διανύσματα), περνούν απο μια σειρά μετασχηματισμών μέχρι να καταλήξουν να προβλήθούν στο επίπεδο της εικόνας (βλ. σχήμα 2).

rendering pipeline

Σχήμα 2: Το rendering pipeline

Κατ' αρχάς όλα τα αντικείμενα είθισται να ορίζονται σε ενα δικό τους σύστημα συντεταγμένων, το οποίο έχει το κέντρο των αξόνων του στο κέντρο του αντικειμένου, η όπου βολέβει καλύτερα κατα περίπτωση, και το οποίο ονομάζεται local coordinate system.

Με κάθε αντικείμενο συσχετίζουμε και έναν μετασχηματισμό που το προσανατολίζει και το τοποθετεί καταλήλως στο ευρύτερο σύστημα συντεταγμένων της σκηνής, το οποίο λέγεται world space.

Κατόπιν ορίζουμε έναν μετασχηματισμό που φέρνει όλα τα αντικείμενα της σκηνής, απο το world space στο view space. Αυτο είναι ενα σύστημα συντεταγμένων, στο οποίο η εικονική "camera" βρίσκεται στο κέντρο των αξόνων και "κοιτάει" προς τον z άξονα. Αυτό το κάνουμε γιατι έτσι πλέον το image plane στο οποίο θέλουμε να προβάλουμε τα πολύγωνά μας, είναι παράλληλο ως προς το xy επίπεδο, κάνοντας την προβολή πολύ πιο απλή μαθηματικά, απο οτι μια προβολή σε ενα τυχαία προσανατολισμένο επίπεδο. Μάλιστα όντας σε view space, μπορούμε να δημιουργίσουμε ισομετρικές προβολές των αντικειμένων αν απλά αγνοήσουμε την συντεταγμένη z.

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

Τελικά έχοντας προβάλει τα vertices των πολυγόνων στο 2D επίπεδο, θεωρούμε το διάστημα [-1, 1] καί στους δύο άξονες ως τα όρια της εικόνας, και το κάνουμε map με το viewport transformation, στα διάστηματα [0, width) και [0, height) οριζόντια και κάθετα αντίστοιχα, ώστε να αποκτίσουμε συντεταγμένες πολυγόνων σε pixels, για να γεμίσουμε μετα τα απαραίτιτα pixels με το κατάληλο χρώμα.

Ώς γνωστόν, γραμμικοί μετασχηματισμοί σε 3-διάστατα συστήματα συντεταγμένων, μπορούν να αναπαραστηθούν με πίνακες 3x3. Όμως ένας πολύ χρήσιμος μετασχηματισμός, η παράλληλη μεταφορά κατα διάνυσμα, δεν είναι γραμμικός μετασχηματισμός, αλλα ανήκει στην ευρύτερη κατηγορία των affine transformations. Για αυτό τον λόγο (και για το projection που προαναφέραμε), χρησιμοποιούμε ομογενείς συντεταγμένες. Δηλαδή αντί για 3D διανύσματα χρησιμοποιούμε 4D διανύσματα με την συντεταγμένη w=1, καθώς και αντίστοιχα 4x4 πίνακες μετασχηματισμού.

Το OpenGL κρατάει σαν μέρος του state του, 4x4 πίνακες για τους παραπάνω μετασχηματισμούς. Το application θέτει κάθε φορά τους κατάλληλους πίνακες στο OpenGL και αυτό αναλαμβάνει να μετασχηματίσει τα vertices που του δίνουμε, με αυτους, κάτι το οποίο συχνά το αναλαμβάνει το hardware, και γίνεται ταχύτατα.

Για να θέσουμε εναν πίνακα στο OpenGL, κατ' αρχάς καλούμε την συνάρτιση glMatrixMode για να ορίσουμε ποιό απο τα matrices θέλουμε να αλλάξουμε, και κατόπιν καλούμε την συνάρτιση glLoadMatrixf, η οποία παίρνει σαν παράμετρο την διεύθυνση ενός array απο 16 floats. Οι παράμετροι της glMatrixMode που μας ενδιαφέρουν είναι οι: GL_MODELVIEW (concatenation των world και view matrices), και GL_PROJECTION. Υπενθύμιση σε όσους δεν θυμούντε καλα γραμμική άλγεβρα, οτι ο συνδιασμός δυο μετασχηματισμών (concatenation), γίνεται με πολλαπλασιασμό των δυο πινάκων που τους ορίζουν.

Το viewpoint transformation ορίζεται ξεχωριστά, με το glViewport function, το οποίο παίρνει 4 floats με το origin και το μέγεθος σε pixels της εικόνας. Αυτή την συνάρτιση χρειάζεται να την καλέσουμε κάθε φορά που αλλάζει το μέγεθος του παραθύρου στο οποίο ζωγραφίζουμε (όταν το GLUT καλεί το reshape callback).

Για να μην χρειάζεται συνεχώς να κατασκεβάζουμε πίνακες μετασχηματισμού και να τους ταίζουμε στο OpenGL με την glLoadMatrixf, υπάρχουν διάφορες βοηθητικές συναρτίσεις που μπορούμε να χρησιμοποιήσουμε. Η glLoadIdentity, θέτει έναν μοναδιαίο πίνακα, ενώ οι συναρτίσεις glTranslatef, glRotatef και glScalef, δημιουργούν και πολαπλασιάζουν πάνω απο το current matrix ενα translation (μεταφορά), rotation (περιστροφή γύρο απο διάνυσμα), ή scaling transformation matrix. Προσοχή, ο πολαπλασιασμός πινάκων δέν ειναι αντιμεταθετικός, οπότε η σειρά που καλούμε αυτές τις συναρτίσεις παίζει ρόλο.

Για να ζωγραφίσουμε έναν απλό κύβο όπως θα κάνουμε εντός ολίγου σε αυτό το πρώτο άρθρο, δεν είναι απαραίτιτο να κάνουμε διαχωρισμό του world απο το view matrix. Μπορούμε να αγνοήσουμε εντελός το world space και να δουλέψουμε σε view space. Ας πούμε οτι θέλουμε να ζωγραφίσουμε τον κύβο μας, περιστραμένο κατα 20 μοίρες γύρο απο τον άξονα y του local coordinate system του, και τοποθετιμένο 5 μονάδες μακρυά στον z άξονα ώστε να τον βλέπει η camera που όπως είπαμε βρίσκεται στο origin και κοιτάει προς το -z. Τότε θα πρέπει να καλέσουμε τις εξής συναρτίσεις πρίν δώσουμε τα πολύγωνα του κύβου στο OpenGL:

glMatrixMode(GL_MODELVIEW); /* the following will affect the modelview matrix */
glLoadIdentity();        /* reset the transformation matrix to identity */
glTranslatef(0, 0, -5);  /* translate 5 units towards the -z axis */
glRotatef(20, 0, 1, 0);  /* rotate 20deg around the y axis */

Επίσης πρέπει να θέσουμε και ενα κατάληλο projection matrix. Τα καλά νέα είναι οτι δεν χρειάζεται να το υπολογίσουμε και να το θέσουμε με την glLoadMatrixf, αφού υπάρχει μια βολική συνάρτιση στην βοηθιτική βιβλιοθήκη GLU, που πάντα έρχεται με το OpenGL η οποία κατακευάζει τον πίνακα και τον πολαπλασιάζει στον current. Η πρώτη παράμετρος της gluPerspective, είναι το κάθετο πεδίο θέασης σε μοίρες, η δεύτερη παράμετρος ειναι ο λόγος πλάτους προς μήκους της εικόνας (οστε να υπολογίσει το οριζόντιο πεδιο θέασης), ενω οι δυο τελευταίες παράμετροι ορίζουν τα near και far clipping planes. Οτιδήποτε είναι πιο κοντά απο το near και πιο μακρύα απο το far clipping plane σε view space, κόβονται. Αυτό είναι απαραίτιτο κατ'αρχάς για να μήν ζωγραφιστούν αντικείμενα που είναι πίσω απο το viewpoint, αλλά και για την λειτουργεία του z-buffering, για το οποίο θα μιλήσουμε σε λίγο. Μιάς και το projection matrix μας όπως είπαμε εξαρτάται απο το μέγεθος του παραθύρου στο οποίο ζωγραφίζουμε, μια καλή θέση για αυτό το call είναι το reshape callback. Αυτή η συνάρτηση καλείται εγγυημένα απο το GLUT και στην αρχή του προγράμματος μας μόλις δημιουργιθεί το παράθυρο, οπότε θα κληθεί οπωσδήποτε πρίν ζωγραφίσουμε για πρώτη φορα.

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0, (float)x / (float)y, 1.0, 1000.0);

Είμαστε έτοιμοι λοιπόν να δώσουμε τα πολύγωνα του κύβου στο OpenGL. Αυτό θα το κάνουμε με μία σειρά απο glVertex3f κλήσεις που ορίζουν τα διανύσματα θέσης των κορυφών των πολυγόνων του κύβου στο local coordinate system. Αυτές τις κλήσεις θα τις κάνουμε μέσα σε ενα glBegin/glEnd block, περνώντας στην glBegin την παράμετρο GL_QUADS, για να καθορίσουμε οτι τα vertices που δίνουμε θέλουμε να ομαδοποιηθούν ανα 4 σε τετράπλευρα πολύγωνα (quadrilaterals). Μαζί με τα διανύσματα θέσης, τα vertices στο OpenGL κουβαλάνε και άλλες ιδιότητες, όπως ενα χρώμα για κάθε vertex. Αυτές τις πληροφορίες τις δίνουμε στο OpenGL με παρόμοια calls (π.χ. glColor3f), και ισχήουν για όλα τα verices απο το σημείο που δόθηκαν και μετά, μέχρι να αλαχτούν. Έτσι στον κύβο μας επίσης δίνουμε και απο ενα χρώμα σε κάθε πλευρά, καλώντας την glColor3f με τις ίδιες 3 παραμέτρους red, green και blue για κάθε vertex της πλευράς αυτής.

Προσθέτουμε λοιπών τον παρακάτω κώδικα στην display συνάρτιση μας, μετά τα glRotate/glTranslate calls που ορίζουν τον μετασχηματισμό του κύβου μας, αλλα πρίν την glutSwapBuffers που δείχνει το αποτέλεσμα.

glBegin(GL_QUADS);
/* far face (-Z) */
glColor3f(0, 0, 1);
glVertex3f(1, -1, -1);
glVertex3f(-1, -1, -1);
glVertex3f(-1, 1, -1);
glVertex3f(1, 1, -1);
/* top face (+Y) */
glColor3f(0, 1, 1);
glVertex3f(-1, 1, 1);
glVertex3f(1, 1, 1);
glVertex3f(1, 1, -1);
glVertex3f(-1, 1, -1);
/* bottom face (-Y) */
glColor3f(1, 0, 1);
glVertex3f(-1, -1, -1);
glVertex3f(1, -1, -1);
glVertex3f(1, -1, 1);
glVertex3f(-1, -1, 1);
/* right face (+X) */
glColor3f(0, 1, 0);
glVertex3f(1, -1, 1);
glVertex3f(1, -1, -1);
glVertex3f(1, 1, -1);
glVertex3f(1, 1, 1);
/* left face (-X) */
glColor3f(1, 1, 0);
glVertex3f(-1, -1, -1);
glVertex3f(-1, -1, 1);
glVertex3f(-1, 1, 1);
glVertex3f(-1, 1, -1);
/* near face (+Z) */
glColor3f(1, 0, 0);
glVertex3f(-1, -1, 1);
glVertex3f(1, -1, 1);
glVertex3f(1, 1, 1);
glVertex3f(-1, 1, 1);
glEnd();

Δοκιμάστε να τρέξετε τον κώδικα, και θα πρέπει να δέιτε την κόκκινη πλευρά του κύβου και λίγο απο την κίτρινη λόγο της περιστροφής 20 μοιρών γύρο απο τον y άξονα όπως ορίσαμε (βλ. εικόνα 1). Για να κάνετε compile το παραπάνω θα χρειαστεί επίσης το -lGLU linker flag μιάς και πρέπει να γίνει link και η βοηθιτική βιβλιοθήκη GLU για το gluPerspective call που χρησιμοποιήσαμε.

3D cube

Εικόνα 1: Ένας 3D κύβος, το hello world του OpenGL

Ας δούμε τώρα τί πρέπει να κάνουμε για να μπορούμε να περιστρέφουμε τον κύβο interactively με το πληκτρολόγιο. Θα χρησιμοποιήσουμε τα πλήκτρα <, > για να περιστρέψουμε τον κύβο μας δεξιόστροφα ή αριστερόστροφα κατα βούλιση. Για να το πετύχουμε αυτο πρέπει καταρχάς να φτιάξουμε μια global μεταβλητή που θα κρατάει την παρούσα γωνία του κύβου, την οποία θα αυξομειόνουμε με το πάτιμα αυτων των δυο πλήκτρων, και θα χρησιμοποιήσουμε αυτή την μεταβλιτή στο glRotatef call, αντί για την σταθερή γωνία των 20 μοιρών.

Οπότε, προσθέστε την εξής μεταβλητή στην αρχή του προγράμματος μας, πρίν την συνάρτηση main:

float angle;

Αλλάξτε το glRotatef call σε:

glRotatef(angle, 0, 1, 0);

Και αλλάξτε την συνάρτιση keyb ως εξής:

void keyb(unsigned char key, int x, int y)
{
    switch(key) {
    case ',':
        angle -= 2;
        glutPostRedisplay();
        break;

    case '.':
        angle += 2;
        glutPostRedisplay();
        break;
    }
}

Οι κλήσεις στην glutPostRedisplay λένε στο GLUT οτι θέλουμε να ξαναζωγραφιστεί η εικόνα, ώστε να δούμε τις αλλαγές. Το GLUT με την σειρά του καλεί την display συνάρτηση μας, όπου θέτουμε το σωστό rotation και ξανα-ζωγραφίζουμε τον κύβο.

Αν δοκιμάσετε το πρόγραμμα σε αυτή την φάση, θα διαπιστόσετε οτι κάτι πάει πολύ στραβά όταν περιστραφέι αρκετά ο κύβος ώστε οι πίσω πλευρές να έρθουν μπροστά (βλ. εικόνα 2). Το πρόβλημα έγγειτε στο ότι το OpenGL ακολουθεί πιστά τις οδηγείες μας, και ζωγραφίζει τα πολύγωνα ακριβώς με την σειρά που του λέμε, έτσι το κόκκινο πολύγωνο, που μετά την περιστροφή είναι πίσω απο το μπλέ, ζωγραφίζεται τελευταίο όπως ορίσαμε στο glBegin/glEnd block, και έτσι γράφεται πάνω απο το μπλέ.

rotated cube

Εικόνα 2: Περιστρέφωντας τον κύβο, το πρόβλημα είναι προφανές.

Λύσεις σε αυτό το πρόβλημα, το οποίο ονομάζεται visible surface determination, υπάρχουν διάφορες, με προτερήματα και μειωνεκτήματα η κάθε μια. Για παράδειγμα θα μπορούσαμε να αποθηκεύσουμε τα πολύγωνα μας σε ένα array, να κάνουμε sort το array αυτό κατα βάθος σε κάθε frame, και να ζωγραφίσουμε τα πολύγωνα back-to-front. Αυτό είναι γνωστό ως το painter's algorithm, γιατί μοίαζει με τον τρόπο που ο ζωγράφος ζωγραφίζει τα αντικείμενα στον καμβά, και αν και δουλεύει για την περίπτωση μας, σπάει σε πολύπλοκες σκηνές με αντικείμενα που τέμνουν το ένα, το άλλο.

Η πίο απλή και καθολική λύση στο visible surface determination problem, και την οποία θα χρησιμοποιήσουμε, λέγεται z-buffering, και δουλεύει ως εξής: Για κάθε pixel της εικόνας, κρατάμε μια τιμή σε ένα ξεχοριστό buffer (zbuffer ή depth buffer), στο οποίο γράφουμε το βάθος του pixel στην εικόνα (το z coordinate σε view space). Μετά, κατα το γέμισμα των πολυγόνων, υπολογίζουμε σε κάθε pixel του πολυγώνου το βάθος του (με linear interpolation των z των τριών vertices), και το συγκρίνουμε με το z που έχει ήδη γραφτεί στον zbuffer για αυτό το pixel προηγουμένως. Αν το z του συγκεκριμένου pixel του πολυγώνου είναι μικρότερο απο αυτό του zbuffer, μόνο τότε γράφουμε το pixel αυτό στην εικόνα. Αν είναι μεγαλύτερο, σημαίνει οτι εχει ήδη γραφτεί κάτι που είναι πιο κόντα στον θεατή προηγουμένως οπότε δεν το κάνουμε overwrite. Φυσικά πρίν αρχίσουμε να ζωγραφίζουμε οτιδίποτε πρέπει να κάνουμε initialize τον depth buffer στην τιμή που αντιστοιχεί στο μακρύτερο δυνατό σημείο.

Όλα τα παραπάνω φυσικά τα αναλαμβάνει το OpenGL, εμείς αρκεί να ενεργοποιήσουμε το GL_DEPTH_TEST, και να καθαρίσουμε τον depth buffer στην αρχή του κάθε frame.

Οπότε, προσθέστε κάπου, π.χ. στην main, πρίν το glutMainLoop call, το εξής:

glEnable(GL_DEPTH_TEST);

Και αλλάξτε το glClear call στην αρχή της display συνάρτησης σε:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Αν ξανατρέξετε τώρα το πρόγραμμα, θα πρέπει ο κύβος να φαίνετε σωστά απο κάθε γωνία (βλ. εικόνα 3).

correct rotated cube

Εικόνα 3: Πλέον ο κύβος φαίνεται σωστός απο όλες τις πλευρές, χάρη στο z-buffering.

Εδώ τελειώνει αυτό το πρώτο μέρος της σειράς OpenGL tutorials, στο επόμενο τεύχος θα μιλήσουμε για το matrix stack το οποίο θα μας βοηθήσει να ξεχωρίσουμε το world απο το view transformation του modelview matrix, και να τοποθετήσουμε περισσότερα αντικείμενα στην σκηνή, καθώς και για τον φωτισμό των αντικειμένων απο φωτεινές πηγές που μπορούμε να ορίσουμε για μεγαλύτερο ρεαλισμό.

Ston teliko kodika aytou tou tutorial, dokimaste na peristrepsete ton kyvo kai 25 moires gyro apo ton x aksona (1 0 0). Deite ti symbainei an topothetisete to kainourio glRotatef call prin i meta apo to yparxon. I antimetathetiki idiotita den isxiei ston polaplasiasmo pinakon.