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

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

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

Με το μοντέλο φωτισμού του Phong που παρουσιάσαμε στο προηγούμενο άρθρο, πλησιάσαμε ενα βήμα πιο κοντά στον φωτορεαλισμό, σκιάζωντας τα αντικείμενα μας κατάλληλα ώστε να προσδώσουμε την ψευδαίσθηση οτι φωτίζονται απο μια εικονική πηγή φωτός. Το επόμενο trick που θα εφαρμόσουμε αυτή τη φορά για να δώσουμε μια ακόμα δόση ρεαλισμού στα αντικείμενα μας, λέγεται texture mapping.

texture mapping

Το texture mapping είναι μια τεχνική που μας επιτρέπει να "τυλίξουμε" μια 3D επιφάνεια, με μια εικόνα (π.χ. μια φωτογραφία) ώστε να φανεί πολύ πιο πολύπλοκη οπτικά, απο ότι πραγματικά είναι. Για παράδειγμα, θα μπορούσαμε να τυλίξουμε μια φωτογραφία της γής απο το διάστημα πάνω σε μια σφαίρα, ώστε να πετύχουμε μια αρκετά πειστική αναπαράσταση της γής.

Το texture mapping, δουλεύει ώς εξής: Ορίζουμε ενα νέο 2-διάστατο σύστημα συντεταγμένων (s, t) πάνω στην εικόνα που θέλουμε να χρησιμοποιήσουμε, έτσι ώστε και οι δύο συντεταγμένες να πηγαίνουν απο 0 έως 1 στα όρια της εικόνας.

Κατόπιν ορίζουμε μια απεικόνιση κάθε σημείου της 3D επιφάνειας που ζωγραφίζουμε, σε αυτό το σύστημα συντεταγμένων, έτσι ώστε να μπορούμε για οποιοδήποτε σημείο της επιφάνειας να βρούμε το σημείο του texture που του αντιστοιχεί, και να πάρουμε το χρώμα της εικόνας σε αυτό το σημείο. Για να το πετύχουμε αυτό, συμπεριλαμβάνουμε άλλη μια πληροφορία σε κάθε κορυφή, κάθε πολυγώνου των αντικειμένων μας, εκτός απο την θέση, το normal, και το χρώμα που είπαμε προηγουμένως: ενα σετ απο (s, t) συντεταγμένες που μας λένε σε ποιό σημείο του συστήματος συντεταγμένων του texture, αντιστοιχεί η κορυφή (βλ. σχήμα 1).

sxima1

Έχοντας ορίσει τα παραπάνω, κατα το ζωγράφισμα κάθε πολυγώνου, υπολογίζουμε (με interpolation), απο τις texture συντεταγμένες της κάθε κορυφής του πολυγώνου, τις συντεταγμένες που αντιστοιχούν στο κάθε pixel που πάμε να γεμίσουμε. Έχοντας αυτό το σετ απο texture συντεταγμένες, κοιτάμε το χρώμα της εικόνας του texture σε αυτές τις συντεταγμένες, και το πολλαπλασιάζουμε στο χρώμα που θα βάφαμε το pixel. Αυτό έχει ως αποτέλεσμα, να χρησιμοποιούμε μεν το χώμα που μας δίνει το texture σε κάθε σημείο, αλλα να επιρεάζεται και απο τον φωτισμό που έχουμε προηγουμένως υπολογίσει με το phong μοντέλο φωτισμού.

texture sampling

Όταν έρθει η ώρα να ανακαλέσουμε το χρώμα που αντιστοιχεί απο ενα texture, έχουμε μια σειρά απο επιλογές για το πώς θα το κάνουμε αυτό.

Η πιο απλή μέθοδος είναι να πάρουμε απλά το κοντινότερο pixel στις texture συντεταγμένες μας και να χρησιμοποιήσουμε το χρώμα του. Η μέθοδος αυτή (nearest ή point sampling) είναι πολύ γρήγορη, αλλα έχει το μειονέκτημα οτι αμα πλησιάσουμε πολυ στην textured επιφάνεια και απλοθεί το texture σε μεγάλο μέρος της εικόνας, αρχίζουμε να βλέπουμε ξεκάθαρα τα pixels σαν μεγάλα μονόχρωμα τετράγωνα πάνω στην επιφάνεια (βλ. εικόνα 1.α).

Αυτό που μπορούμε να κάνουμε για να βελτιόσουμε την ποιότητα του αποτελέσματος όταν η εικόνα του texture μεγαλώσει πολύ είναι να μήν πάρουμε μόνο το κοντινότερο pixel στις συντεταγμένες που μας ενδιαφέρουν, αλλα και άλλα έναν μέσο όρο των διπλανών pixels. Αυτό έχει ως αποτέλεσμα να θολώσει ελαφρώς η εικόνα, αλλα αυτο είναι λιγότερο ενοχλητικό στο μάτι απο τον τετραγωνισμό της προηγούμενης περίπτωσης, και για λογικές μεγενθήσεις περνάει απαρατήρητο (βλ. εικόνα 1.β).

eikona1

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

OpenGL textures

Το πρώτο βήμα για να χρησιμοποιήσουμε texture mapping στο OpenGL, είναι να δώσουμε κάπως στο OpenGL τα pixels της εικόνας που θέλουμε να χρησιμοποιήσουμε ως texture. Για να το πετύχουμε αυτό, το OpenGL μας παρέχει την συνάρτηση glTexImage2D, η οποία παίρνει σαν παραμέτρους το μέγεθος της εικόνας σε pixels στις δύο διαστάσεις, το format στο οποίο θέλουμε να κρατήσει την εικόνα εσωτερικά (π.χ. 24bit RGB, 32bit RGBA, 8bit luminance, κ.α.), ενα pointer στο array απο pixels απο όπου θέλουμε να διαβάσει την εικόνα, και τι format έχουν εκει μέσα τα pixels μας (π.χ. 3 chars που αντιστοιχούν σε RGB, 4 floats που αντιστοιχούν σε RGBA κλπ). Επιπλέον, η συνάρτηση αυτη μας επιτρέπει να ορίσουμε κάποιες επιπλέων παραμέτρους όπως σε ποιό mip-map level αναφερόμαστε, κάτι για το οποίο δέ θα μιλήσουμε στο παρόν άρθρο και θα το θέτουμε πάντα 0 (πρώτο επίπεδο), και εαν θέλουμε ενα πλαίσιο συγκεκριμένου χρώματος στην άκρη της εικόνας, κατι το οποίο χρησιμοποιείται σπανίως και επίσης δεν θα αναφερθούμε περετέρο σε αυτό.

Για παράδειγμα, αν έχουμε μια εικόνα 256x128 pixels που θέλουμε να χρησιμοποιήσουμε σαν texture, με 3 bytes ανα pixel (RGB), τότε καλούμε την glTexImage2D ως εξής:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 128, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels);
Η πρώτη παράμετρος απλα λέει οτι θέτουμε ενα 2-διάστατο texture (υπάρχουν μονοδιάστατα, 3-διάστατα, και άλλοι τύποι που ξεφεύγουν απο το πλαίσιο αυτου του άρθρου). Η δεύτερη παράμετρος ειναι το mipmap επίπεδο που θέτουμε, προς το παρόν πάντα 0. H τρίτη παράμετρος ειναι το format στο οποίο θέλουμε το OpenGL να κρατήσει το texture εσωτερικά. Η τέταρτη και πέμπτη παράμετρος ειναι το πλάτος και το ύψος της εικόνας. H έκτη παράμετρος ειναι το πλαίσιο που δέν θέλουμε και το αγνοούμε. Οι επόμενες 2 παράμετροι λένε σε τι format έχουμε εμείς τα pixels που δίνουμε στην συνάρτηση όστε να τα διαβάσει σωστά απο το array μας, και η τελευταία παράμετρος ειναι το pointer στο array με τα pixels.

Επίσης πρέπει να πούμε στο OpenGL τί μέθοδο sampling να χρησιμοποιήσει. Όπως είπαμε παραπάνω θέλουμε να παίρνει τον μέσο όρο απο γειτονικά pixels, το οποίο λέγεται linear filtering.

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

Τέλος πρέπει να ενεργοποιήσουμε το texture mapping με την συνάρτηση:

glEnable(GL_TEXTURE_2D);
πρίν ζωγραφίσουμε το αντικείμενο στο οποίο θέλουμε να χρησιμοποιήσουμε αυτό το texture.

Εδώ πρέπει να πούμε οτι υπάρχει ενας περιορισμός όσον αφορά τις εικόνες που μπορούμε να χρησιμοποιήσουμε σαν textures. Μόνο μεγέθη τα οποία είναι δύναμη του 2 μπορούν να χρησιμοποιηθούν. Έτσι μπορούμε να έχουμε εικόνες π.χ. 256x256, 64x128, 1024x512, αλλα όχι 800x600, 1024x768 ή οτιδίποτε άλλο δεν είναι δύναμη του 2 και στις δύο διαστάσεις.

texture συντεταγμένες

Όπως είπαμε προηγουμένως, για να κάνουμε map ενα texture στην επιφάνεια τον αντικειμένων μας, χρειαζόμαστε μια επιπλέων πληροφορία σε κάθε vertex: τις συντεταγμένες του texture που αντιστοιχούν σε αυτό το vertex. Αυτό το κάνουμε προσθέτωντας μέσα στο glBegin/glEnd block, πρίν απο κάθε glVertex3f call, και μια κλήση στην glTexCoord2f, που παίρνει 2 παραμέτρους: τις συντεταγμένες (s, t).

αρχεία εικόνας

Το OpenGL όπως είδαμε παραπάνω απλα περιμένει απο εμάς ενα pointer σε μια αράδα απο pixels. Δέν ξέρει απο αρχεία εικόνων και δέν μπορέι να φωρτώσει εικόνες απο το filesystem. Αυτά είναι δικία μας ευθήνη, αν θέλουμε να διαβάσουμε τα pixels απο κάποιο αρχείο θα πρέπει να φροντίσουμε να γράψουμε τον απαραίτητο κώδικα που να διαβάζει κάποιο image file format. Ένα πολύ απλό και διαδεδομένο τέτοιο format, έιναι το "portable pixmap" (ppm). Μπρορείτε να μετατρέψετε οποιαδήποτε εικόνα σε ppm απο το gimp, το imagemagick, κλπ.

To portable pixmap format είναι υπερβολικά απλό. Αποτελείτε απο εναν header απλού κειμένου σε 3 γραμμές που περιέχουν: το διακριτικό P6, το πλάτος και το ύψος της εικόνας, και την μέγιστη τιμή που μπορεί να πάρει ενα απο τα 3 RGB συστατικά του pixel (συνήθος 255). Μετα τον header ακολουθούν width*height*3 bytes με τα pixels της εικόνας (R0 G0 B0 R1 G1 B1 ... Rn Gn Bn).

Ας δούμε λοιπόν ένα παράδειγμα οπου φωρτώνουμε μια εικόνα απο το filesystem, και την χρησιμοποιούμε σαν texture σε ένα quad (βλ. εικόνα 2).

eikona2

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <GL/glut.h>

void display(void);
unsigned char *load_ppm(const char *fname, int *xsz, int *ysz);
void reshape(int x, int y);

/* texture width, height, and pixel array pointer */
int width, height;
unsigned char *pixels;

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 handlers */
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);

    /* load the pixmap into an array of pixels */
    if(!(pixels = load_ppm("opengl.ppm", &width, &height))) {
        return 1;
    }
    /* set the texture's pixel data, dimensions and format */
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
            GL_RGB, GL_UNSIGNED_BYTE, pixels);
    /* set texture sampling method */
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    /* enable texture mapping */
    glEnable(GL_TEXTURE_2D);

    glutMainLoop();
    return 0;
}

/* display callback */
void display(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_MODELVIEW);
    /* set view matrix */
    glLoadIdentity();
    gluLookAt(2, 2, 4, 0, 0, 0, 0, 1, 0);

    glBegin(GL_QUADS);
    glTexCoord2f(0, 1);
    glVertex3f(-1, -1, 0);
    glTexCoord2f(1, 1);
    glVertex3f(1, -1, 0);
    glTexCoord2f(1, 0);
    glVertex3f(1, 1, 0);
    glTexCoord2f(0, 0);
    glVertex3f(-1, 1, 0);
    glEnd();

    glutSwapBuffers();
}

/* this function loads a ppm file, and returns a pointer to a newly
 * allocated array of pixels. width & height are returned through the
 * xsz and ysz pointers.
 */
unsigned char *load_ppm(const char *fname, int *xsz, int *ysz)
{
    FILE *fp;
    int i, width, height, hdrline = 0;
    unsigned char *pixels = 0;

    if(!(fp = fopen(fname, "rb"))) {
        fprintf(stderr, "failed to open pixmap: %s: %s\n", fname, strerror(errno));
        return 0;
    }
    /* read ppm header */
    while(hdrline < 3) {
        char buf[64];

        if(!fgets(buf, sizeof buf, fp))
            goto err;    
        /* skip comments */
        if(buf[0] == '#')
            continue;

        switch(hdrline++) {
        case 0:
            /* first header line should be P6 */
            if(strcmp(buf, "P6\n") != 0)
                goto err;
            break;
        case 1:
            /* second header line contains the pixmap dimensions */
            if(sscanf(buf, "%d %d", &width, &height) != 2)
                goto err;
            break;
        }
    }
    /* allocate the image (each pixel is 3 bytes r, g, and b) */
    if(!(pixels = malloc(width * height * 3))) {
        goto err;
    }
    /* read all pixels */
    for(i=0; i<width * height * 3; i++) {
        int c = fgetc(fp);
        if(c < 0)
            goto err;
        pixels[i] = c;
    }
    fclose(fp);
    *xsz = width;
    *ysz = height;
    return pixels;

err:
    fprintf(stderr, "failed to load pixmap: %s\n", fname);
    free(pixels);
    fclose(fp);
    return 0;
}

/* reshape callback (window is resized) */
void reshape(int x, int y)
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0, (float)x / (float)y, 1.0, 1000.0);
}

Προηγουμένος είπαμε πει οτι κάνουμε map, το κάθε vertex στο σύστημα συντεταγμένων του texture με της συντεταγμένες να κυμαίνονται απο 0 έως 1. Η αλήθεια είναι οτι εμείς μπορούμε να δώσουμε ότι συντεταγμένες θέλουμε, και το OpenGL θα τις κάνει map στο διάστημα [0, 1] με έναν απο δύο πιθανούς τρόπους:

Στην πρώτη περίπτωση, η οποία είναι και το default, αν δεν ορίσουμε το αντίθετο, ουσιαστικά το texture επαναλαμβάνεται πολλές φορές πάνω στην επιφάνεια που ζωγραφίζουμε (εικόνα 3.a), ενώ στην δεύτερη περίπτωση βλέπουμε μόνο μία φόρα την εικόνα στην επιφάνεια στο κομμάτι αυτής που περιέχει συντεταγμένες στο διάστημα [0, 1], ενώ απο 'κει και πέρα επαναλαμβάνεται επ'άπειρον η τελευταία σειρά η στήλη απο pixels της εικόνας (εικόνα 3.β).

eikona3
Όταν θέλουμε να πετύχουμε το δεύτερο αποτέλεσμα, πρέπει να πούμε στο OpenGL να κάνει clamp, τα texture coordinates στο διάστημα [0, 1] (GL_CLAMP), αντι για το default GL_REPEAT.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

texture objects

Το να δίνουμε συνεχός το array με τα pixels στο OpenGL πρίν ζωγραφίσουμε ενα αντικείμενο είναι χρονοβόρο, καθός το OpenGL πρέπει να αντιγράψει κάθε φωρά τα pixels στην μνήμη της κάρτας γραφικών. Γιαυτό στο OpenGL 1.1 προστέθικαν τα texture objects. Η ιδέα είναι οτι το OpenGL κρατάει εσωτερικά μια λίστα απο textures οπου για το καθένα διατηρεί τα pixels του, παραμέτρους sampling κλπ, και εμείς αναφερόμαστε στο κάθε texture με έναν αριθμό. Έτσι, κάθε φωρά που θέλουμε να ζωγραφίσουμε ενα αντικείμενο με κάποιο texture, αντί να δίνουμε τα pixels του εξάρχης, απλά λέμε στο OpenGL ποιό texture θέλουμε να χρησιμοποιήσει δίνοντας το νούμερο που αντιστιχεί σε αυτό στην συνάρτηση glBindTexture.

Για να δημιουργίσουμε ενα texture object, καλούμε κατάρχας την συνάρτιση glGenTextures, ώστε να πάρουμε ενα αχρισιμοποίητο μεχρι στιγμής νούμερο, το κάνουμε bind, και ορίζουμε pixels και άλλες παραμέτρους κανονικά.

Για να μετατρέψουμε το προηγούμενο παράδειγμα όστε να χρησιμοποιήσουμε texture objects, καταρχάς προσθέτουμε μια global μεταβλιτή για να κρατίσουμε το texture id:

unsigned int tex_id;
Και προσθέτουμε τις εξής γραμμές μετά την κλήση της load_ppm, και πρίν την glTexImage2D:
glGenTextures(1, &tex_id);
glBindTexture(GL_TEXTURE_2D, tex_id);
Τώρα όποτε θέλουμε να ζωγραφίσουμε αντικέιμενο με αυτό το texture, απλά καλούμε πρώτα:
glBindTexture(GL_TEXTURE_2D, tex_id);

Ας κάνουμε όμως το δεύτερο παράδειγμα λίγο πιο ενδιαφέρον. Αυτή τη φορά θα ζωγραφίσουμε ενα μεταλικό πάτομα (ενα quad με το κατάλληλο texture), και πάνω σε αυτό, ενα ξύλινο κυβότιο (έναν απλό κύβο με το κατάληλο texture), ώστε να δούμε πως το texture mapping μπορεί να προσδόσει ενα μεγάλο ποσοστό ρεαλισμού στα αντικείμενα μας με πολύ απλό τρόπο.

Αφαιρούμε απο τις global μεταβλιτές τα width, height, και pixels, μιας και δεν τα χρειαζώμαστε πια εκεί, και προσθέτουμε 2 μια global μεταβλιτές για τα textures του κυβοτίου, και του εδάφους:

unsigned int tex_ground, tex_crate;

Για ευκολία ας φτιάξουμε μια καινούρια συνάρτηση που θα παίρνει σαν παράμετρo ενα όνομα αρχείου, θα φορτώνει την εικόνα, θα φτιάχνει ενα OpenGL texture object και θα μας επιστρέφει το id του.

unsigned int load_texture(const char *fname)
{
    int width, height;
    unsigned char *pixels;
    unsigned int tex_id;

    if(!(pixels = load_ppm(fname, &width, &height))) {
        return 0;
    }
    glGenTextures(1, &tex_id);
    glBindTexture(GL_TEXTURE_2D, tex_id);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
            GL_RGB, GL_UNSIGNED_BYTE, pixels);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    free(pixels); /* not needed any more */
    return tex_id;
}

Στην main, αντικαθιστούμε το κομμάτι που φωρτώνει το texture, με:

tex_ground = load_texture("ground.ppm");
tex_crate = load_texture("crate.ppm");
if(!tex_logo || !tex_crate) {
    return 1;
}
Και ενεργοποιούμε texture mapping, φωτισμό, και z-buffering:
glEnable(GL_TEXTURE_2D);
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);

Φτιάχνουμε μια καινούρια συνάρτηση που ορίζει τα πολύγωνα ενος κύβου με normals και texture coordinates:

void draw_cube(void)
{
    glBegin(GL_QUADS);
    /* far face (-Z) */
    glNormal3f(0, 0, -1);
    glTexCoord2f(1, 1);    glVertex3f(1, -1, -1);
    glTexCoord2f(0, 1);    glVertex3f(-1, -1, -1);
    glTexCoord2f(0, 0);    glVertex3f(-1, 1, -1);
    glTexCoord2f(1, 0);    glVertex3f(1, 1, -1);
    /* top face (+Y) */
    glNormal3f(0, 1, 0);
    glTexCoord2f(0, 1); glVertex3f(-1, 1, 1);
    glTexCoord2f(1, 1); glVertex3f(1, 1, 1);
    glTexCoord2f(1, 0); glVertex3f(1, 1, -1);
    glTexCoord2f(0, 0); glVertex3f(-1, 1, -1);
    /* bottom face (-Y) */
    glNormal3f(0, -1, 0);
    glTexCoord2f(0, 1); glVertex3f(-1, -1, -1);
    glTexCoord2f(1, 1); glVertex3f(1, -1, -1);
    glTexCoord2f(1, 0); glVertex3f(1, -1, 1);
    glTexCoord2f(0, 0); glVertex3f(-1, -1, 1);
    /* right face (+X) */
    glNormal3f(1, 0, 0);
    glTexCoord2f(0, 1); glVertex3f(1, -1, 1);
    glTexCoord2f(1, 1); glVertex3f(1, -1, -1);
    glTexCoord2f(1, 0); glVertex3f(1, 1, -1);
    glTexCoord2f(0, 0); glVertex3f(1, 1, 1);
    /* left face (-X) */
    glNormal3f(-1, 0, 0);
    glTexCoord2f(0, 1); glVertex3f(-1, -1, -1);
    glTexCoord2f(1, 1); glVertex3f(-1, -1, 1);
    glTexCoord2f(1, 0); glVertex3f(-1, 1, 1);
    glTexCoord2f(0, 0); glVertex3f(-1, 1, -1);
    /* near face (+Z) */
    glNormal3f(0, 0, 1);
    glTexCoord2f(0, 1); glVertex3f(-1, -1, 1);
    glTexCoord2f(1, 1); glVertex3f(1, -1, 1);
    glTexCoord2f(1, 0); glVertex3f(1, 1, 1);
    glTexCoord2f(0, 0); glVertex3f(-1, 1, 1);
    glEnd();
}

Τέλος αλλάζουμε την display ως εξής:

void display(void)
{
    float lpos[] = {8, 10, 15, 1};

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_MODELVIEW);

    /* set view matrix */
    glLoadIdentity();
    gluLookAt(2, 2, 4, 0, 0, 0, 0, 1, 0);

    /* set light position */
    glLightfv(GL_LIGHT0, GL_POSITION, lpos);

    /* bind crate texture and render cube */
    glBindTexture(GL_TEXTURE_2D, tex_crate);
    draw_cube();

    /* bind ground texture and render ground quad */
    glBindTexture(GL_TEXTURE_2D, tex_ground);
    glBegin(GL_QUADS);
    glNormal3f(0, 1, 0);
    glTexCoord2f(0, 4);
    glVertex3f(-4, -1, -4);
    glTexCoord2f(4, 4);
    glVertex3f(4, -1, -4);
    glTexCoord2f(4, 0);
    glVertex3f(4, -1, 4);
    glTexCoord2f(0, 0);
    glVertex3f(-4, -1, 4);
    glEnd();

    glutSwapBuffers();
}

Το αποτέλεσμα (εικόνα 4) σίγουρα είναι εντυπωσιακό, αν αναλογιστούμε οτι το μόνο που κάναμε είναι να κολίσουμε εικόνες πάνω σε πολύ απλα πολυγονικά αντικείμενα!

eikona4

Μπορείτε να κατεβάσετε τα παραδείγματα αυτού του τεύχους εδώ. Και όπως παντα για οτιδίποτε απορείες η διευκρινίσεις, μην διστάσετε να μου στείλετε email στο nuclear@mutantstargoat.com.

Στο επόμενο τεύχος θα μιλήσουμε για το alpha blending και το alpha testing, που θα μας βοηθήσουν να ζωγραφίσουμε ημι-διάφανα αντικέιμενα στη σκηνή μας, και θα χρησιμοποιήσουμε textures με alpha channel ώστε να ζωγραφίσουμε αντικέιμενα που έχουν διαφανή και αδιαφανή μέρη.
Μέχρι τότε, happy hacking!