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

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

Εισαγωγή

Καλοσήρθατε σε άλλο ένα επισόδειο της εισαγωγής στον προγραμματισμό 3D γραφικών με το OpenGL. Αυτή τη φορά θα μάθουμε για την δυνατότητα να αναμίξουμε τα χρώματα διαφορετικών πολυγόνων στον framebuffer χρησιμοποιώντας τις διάφορες blend functions που παρέχει το OpenGL. Θα δούμε πως μπορούμε να προσεγγύσουμε την εμφάνιση ημιδιαφανών επιφανειών, χρησιμοποιώντας alpha blending, και πώς μπορούμε να ορίσουμε το ποσό της διαφάνειας σε επίπεδο αντικειμένου, αλλά και σε επίπεδο pixel, χρησημοποιώντας textures με alpha channel.

Blending

Το blending δεν είναι τίποτε άλλο απο ένα σταθμισμένο άθροισμα του χρώματος των pixels που υπάρχουν στον framebuffer, με τα pixels που πάν να γραφτούν ώς αποτέλεσμα του rasterization των πολυγώνων που ζωγραφίζουμε. Η μορφή του αθροίσματος αυτού είναι: S * Fs + D * Fd Όπου S είναι το χρώμα του pixel που πάει να γραφτεί, D το χρώμα που υπάρχει σε στο αντίστοιχο pixel του framebuffer και Fs, Fd παράγωντες που πολλαπλασιάζονται με αυτά, και μπορούμε να τους ορίσουμε οστε να πετύχουμε το επιθυμιτό αποτέλεσμα.

Για παράδειγμα, αν θέλουμε να προστεθεί το χρώμα του πολυώνου που ζωγραφίζουμε στα περιεχόμενα του framebuffer, αρκεί να θέσουμε Fs = 1 και Fd = 1 (εικόνα 1.α). Εάν θέλουμε να πολλαπλασιαστεί, τότε μπορούμε να θέσουμε Fs = 0 και Fd = S ή Fs = D και Fd = 0 (εικόνα 1.β). Ενω άν θέλουμε να γίνει ενας γραμμικός συνδιασμός των δύο χρωμάτων με παράμετρο α στο διάστημα [0, 1] θέτουμε Fs = α και Fd = 1 - α (εικόνα 1.γ).

eikona1

Το τελευταίο ιδικά ειναι ιδιεταίρως χρήσιμο, μιας και λέγεται alpha blending, και είναι η μέθοδος που μας επιτρέπει να προσεγγύσουμε την εμφάνιση ημιδιαφανών επιφανειών. Αλλα ας δούμε πρώτα πώς γίνονται τα παραπάνω στο OpenGL.

Το πρώτο πράγμα που πρέπει να κάνουμε στο OpenGL για να χρησιμοποιήσουμε blending, είναι να το ενεργοποιήσουμε καλώντας:

glEnable(GL_BLEND);
Ενώ για να ορίσουμε τις παραμέτρους της παραπάνω συνάρτησης blend καλόύμε την glBlendFunc. Π.χ. για να πετύχουμε additive blending όπως στο σχήμα 1.α με Fs = 1 kai Fd = 1, καλούμε:
glBlendFunc(GL_ONE, GL_ONE);
Όπου η πρώτη παράμετρος ειναι το Fs (source factor) και η δεύτερη το Fd (destination factor).

Ακόλουθεί ένας πίνακας με τα πιο σημαντικά blend factors, που αντιστοιχεί το όνομα με το τί σημάινει:
όνομαfactor
GL_ZERO0
GL_ONE1
GL_DST_COLORD
GL_SRC_COLORS
GL_ONE_MINUS_DST_COLOR1 - D
GL_ONE_MINUS_SRC_COLOR1 - S
GL_SRC_ALPHAα
GL_ONE_MINUS_SRC_ALPHA1 - α

Ο πιό συνηθησμένος συνδιασμός απο blend factors είναι αυτός που χρησιμοποιειται στο alpha blending, δηλαδή S * α + D * (1 - α):

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Το 'α' (alpha) που χρησιμοποιείται παραπάνω είναι ουσιαστικά το τέταρτο στοιχείο του χρώματος που ορίζουμε με διάφορους τρόπους στο OpenGL. Όταν δέν χρησιμοποιούμε φωτισμό, το χρώμα του κάθε vertex, μαζί με το alpha value του προέρχονται απο την κλήση της glColor4f(r, g, b, a). Εάν αντί για την glColor4f καλέσουμε την glColor3f και έτσι ορίσουμε μόνο τα red/green/blue συστατικά του χρώματος και όχι το alpha, τότε το alpha θεωρείται ίσο με 1 (το οποίο σημαίνει οτι καταλίγουμε σε μια παράσταση blending: S * 1 + D * 0 άρα ζωγραφίζεται πλήρος το χρώμα του πολυγόνου και δέν αναμηγνείεται με τα απο πίσο, και δίνεται η ψευδαίσθηση ότι το αντικείμενο που ζωγραφίζουμε δέν είναι διάφανο).

Όταν χρησιμοποιούμε φωτισμό, όπως έχουμε πεί, το χρώμα του αντικειμένου προέρχεται απο το material του (δηλαδή απο την κλήση glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, color), όπου το color είναι ενα array απο 4 floats: r, g, b, a), και έτσι παίρνουμε και το alpha.

Και στις δύο περιπτώσεις εάν χρήσιμοποιούμε κάποιο texture map, και αυτό έχει alpha channel, είναι δηλαδή της μορφής GL_RGBA, τότε το alpha του texture, πολλαπλασιάζεται με το alpha του material ή του vertex, όπως άλλωστε και τα υπόλοιπα 3 στοιχεία του χρώματος. Έτσι αν χρησιμοποιήσουμε ένα texture που έχει κάποια ημιδιαφανή pixels (α < 1) τότε τα αντίστοιχα σημεία του αντικειμένου θα φανούν επίσης ημιδιαφανή (εικόνα 2).

eikona2

Alpha Blending Artifacts

Όταν δοκιμάσουμε να χρησιμοποιήσουμε alpha blended αντικείμενα σε μια πλήρη 3D σκηνή, για να δημιουργήσουμε την αίσθηση της διαφάνειας, θα διαπιστόσουμε μερικά προβλήματα. Κατ'αρχάς αν θυμηθούμε πως δουεύει το z-buffering που είχαμε περιγράψει στο πρώτο άρθρο, για κάθε pixel που θέλουμε να γράψουμε στον framebuffer ελέγχετε το βάθος του σε αντιπαράθεση με το βάθος του pixel που είναι αυτη τη στιγμή στον framebuffer, και ζωγραφίζετε το pixel μόνο άν είναι πιο κοντά στον θεατή. Αυτό σημαίνει οτι αν πάμε να ζωγραφίσουμε ένα ημιδιάφανο πολύγωνο, και μετά ένα άλλο πολύγονο πίσο απο αυτο, το δέυτερο δέν θα ζωγραφιστεί, αφου θα το κρύβει το πρώτο, και έτσι θα χαλάσει η ψευδαίσθηση τις διαφάνειας.

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

Αλλα αυτό δέν μας λύνει το πρόβλημα όταν επικαλύπτονται οπτικά δυο ημιδιαφανή αντικείμενα. Για να καλύψουμε και αυτή την περίπτωση πρέπει να φροντίσουμε να μήν ζωγραφίζουμε κάν τα σχεδών εντελώς διαφανή αντικείμενα ή κομμάτια αντικειμένων κάν, ώστε να μήν γράφεται το βάθος τους στον zbuffer παρόλο που είναι πρακτικά αόρατα. Αυτό το πετυχαίνουμε με το alpha testing. Με τις επόμενες δύο γραμμές λέμε στο OpenGL να απορρίψει οποιοδήποτε pixel πάμε να ζωγραφίσουμε που έχει τιμή alpha κάτο απο 0.05 (πρακτικά αόρατο):

glEnable(GL_ALPHA_TEST);
glAlphaFunc(GL_GEQUAL, 0.05);

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

Σκηνή με Blending

Στο παράδειγμα που ακολουθεί, θα ζωγραφίσουμε μια 3D σκηνή με blended αντικείμενα χρησιμοποιώντας όλες τις παραπάνω τεχνικές εκτός απο την ταξινόμηση των ημιδιαφανών αντικειμένων. Αυτό δέν θα το κάνουμε για εξοικονόμιση χώρου, μιας και στην απλή σκηνή που ζωγραφίζουμε, δέν μπορεί εύκολα να παρατηρηθεί οπτικό λάθος απο επικάλυψη ημιδιαφανών pixels που δέν τα απορρίπτει το alpha test.

Θα ζωγραφίσουμε έναν θόλο για ουρανό χρησιμοποιώντας ενα alpha map συννέφων ώστε πίσω απο τα σύννεφα να φάινετε το μπλέ στό οποίο καθαρίζουμε την οθόνη αρχικά, ένα απλό επίπεδο με texture γρασιδιού για έδαφος, 200 δέντρα σε τυχαίες θέσεις, έναν φράχτη ο οποίος ουσιαστικά είναι ενα απλό κουτί με color texture ξύλου και για alpha μια ασπρόμαυρη εικόνα με τα διαφανή και μή σημεία του φράχτη, και τέλος μια ημιδιαφανή τσαγιέρα. Τα δέντρα είναι απλά δυο πολύγονα (quads) τοποθετημένα σταυροτά, έτσι ώστε απο όπου και αν τα κοιτάξουμε να βλέπουμε εικόνα δέντρου η οποία είναι απλα το texture map που χρησιμοποιύμε όταν τα ζωγραφίζουμε, με το αντίστοιχο alpha map φυσικά ώστε μόνο ο κορμός και τα φύλλα να φαίνονται αδιάφανη (εικόνα 3). Η τσαγιέρα που δέν χρησιμοποιεί textures με alpha channel, παίρνει το ημιδιάφανο (0.5) alpha της απο το τέταρτο στοιχείο του χρώματος που δίνουμε στο material. Τέλος για να ζωγραφιστεί σωστά η τσαγιέρα μιας και ειναι ημιδιάφανη, φρωντίζουμε να ζωγραφίσουμε πρώτα την πίσο πλευρά της χρησιμοποιώντας backface culling, και μέτα την μπροστινή, μιας και όπως είπαμε τα ημιδιαφανή αντικείμενα πρέπει να ζωφραφίζωνται με αυτή τη σειρά.

Στον παρακάτω κώδικα θα χρησιμοποιήσουμε την ίδια συνάρτηση για φώρτωμα εικόνων τύπου PPM (portable pixmap), όπως και την προηγούμενη φορά, με την διαφορά οτι η load_texture συνάρτηση παίρνει σαν παραμέτρους δύο ονόματα αρχείων εικώνας, ένα για το χρώμα (rgb) του texture, και ένα δεύτερο ασπρόμαυρο alpha map (όπου μάυρο σημάινει διαφανές και άσπρο αδιαφανές), και τα συνδυάζει σε ένα RGBA texture.

Ζωγραφίζουμε ένα θόλο

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

void display(void);
void draw_sky(void);
void draw_ground(void);
void draw_tree(void);
void draw_fence(void);
void draw_teapot(void);
unsigned int load_texture(const char *cfname, const char *afname);
unsigned char *load_ppm(const char *fname, int *xsz, int *ysz);
void reshape(int x, int y);

unsigned int tex_grass, tex_fence, tex_tree, tex_clouds;

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);

    tex_grass = load_texture("grass.ppm", 0);
    tex_fence = load_texture("fence.ppm", "fence-alpha.ppm");
    tex_tree = load_texture("tree.ppm", "tree-alpha.ppm");
    tex_clouds = load_texture(0, "clouds-alpha.ppm");

    if(!tex_grass || !tex_fence || !tex_tree || !tex_clouds) {
        fprintf(stderr, "failed to load texture\n");
        return 1;
    }
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);

    glEnable(GL_ALPHA_TEST);
    glAlphaFunc(GL_GEQUAL, 0.05f);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glClearColor(0.55, 0.7, 1, 1);

    glutMainLoop();
    return 0;
}

void display(void)
{
    float lpos[] = {80, 500, 100, 0};

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_MODELVIEW);
    /* set view matrix */
    glLoadIdentity();
    glTranslatef(0, -1, -8);
    glRotatef(5, 1, 0, 0);
    glRotatef(160, 0, 1, 0);

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

    /* draw everything */
    draw_sky();
    draw_ground();
    draw_tree();
    draw_fence();
    draw_teapot();

    glutSwapBuffers();
}

/* draw sky mesh (curved tesselated plane) */
void draw_sky(void)
{
    int i, j, k, sky_subdiv = 20;
    /* don't illuminate the sky */
    glDisable(GL_LIGHTING);

    glBindTexture(GL_TEXTURE_2D, tex_clouds);

    glBegin(GL_QUADS);
    for(i=0; i<sky_subdiv; i++) {
        for(j=0; j<sky_subdiv; j++) {
            int offs[][2] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
            for(k=0; k<4; k++) {
                float x = (float)(i + offs[k][0]) / (float)sky_subdiv;
                float y = (float)(j + offs[k][1]) / (float)sky_subdiv;
                float height = 250.0 * sin(x * M_PI) * sin(y * M_PI);

                glTexCoord2f(x, y);
                glVertex3f(1000.0 * x - 500.0, height - 10, 1000.0 * y - 500.0);
            }
        }
    }
    glEnd();
    glEnable(GL_LIGHTING);
}

/* the ground is just a big quad with a grass texture */
void draw_ground(void)
{
    float color[] = {1, 1, 1, 1};
    glBindTexture(GL_TEXTURE_2D, tex_grass);

    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, color);

    glBegin(GL_QUADS);
    glNormal3f(0, 1, 0);
    glTexCoord2f(0, 0); glVertex3f(-100, 0, -100);
    glTexCoord2f(20, 0); glVertex3f(100, 0, -100);
    glTexCoord2f(20, 20); glVertex3f(100, 0, 100);
    glTexCoord2f(0, 20); glVertex3f(-100, 0, 100);
    glEnd();
}

#define NUM_TREES    200
void draw_tree(void)
{
    static float tpos[NUM_TREES][3];
    static int placed_trees;
    int i;

    glBindTexture(GL_TEXTURE_2D, tex_tree);

    glDisable(GL_LIGHTING);

    for(i=0; i<NUM_TREES; i++) {
        if(!placed_trees) {
            /* place trees randomly */
            tpos[i][0] = (float)rand() / (float)RAND_MAX * 50.0 - 25.0;
            tpos[i][1] = 0.0;
            tpos[i][2] = (float)rand() / (float)RAND_MAX * 50.0 - 25.0;
        }
        glPushMatrix();
        glTranslatef(tpos[i][0], tpos[i][1], tpos[i][2]);

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

        glPopMatrix();
    }
    placed_trees = 1;

    glEnable(GL_LIGHTING);
}

/* the fence is just a wooden box with an alpha map */
void draw_fence(void)
{
    float color[] = {1, 1, 1, 1};
    glBindTexture(GL_TEXTURE_2D, tex_fence);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, color);

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

/* and a semi-transparent object */
void draw_teapot(void)
{
    /* use an alpha = 0.5 in the diffuse material color */
    float color[] = {1.0, 0.7, 0.4, 0.5};
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, color);

    glDisable(GL_TEXTURE_2D);

    glPushMatrix();
    glTranslatef(0, 0.76, 0);

    glEnable(GL_CULL_FACE);
    /* first render the back faces */
    glutSolidTeapot(1.0);
    glFrontFace(GL_CW);
    /* then render the front faces */
    glutSolidTeapot(1.0);
    glFrontFace(GL_CCW);
    glDisable(GL_CULL_FACE);

    glPopMatrix();

    glEnable(GL_TEXTURE_2D);
}

/* load a color image and an alpha grayscale image and produce a single RGBA texture map */
unsigned int load_texture(const char *cfname, const char *afname)
{
    int i, width, height, colx, coly, ax, ay;
    unsigned char *pixels = 0, *rgb = 0, *alpha = 0;
    unsigned int tex_id = 0;

    /* load the color image (if specified) */
    if(cfname && !(rgb = load_ppm(cfname, &colx, &coly))) {
        goto ret;
    }
    /* load the alpha image (if specified) */
    if(afname && !(alpha = load_ppm(afname, &ax, &ay))) {
        goto ret;
    }
    /* make sure both are the same size */
    if(rgb && alpha && (colx != ax || coly != ay)) {
        fprintf(stderr, "colormap and alphamap must be of the same size\n");
        goto ret;
    }
    /* allocate RGBA image memory */
    width = rgb ? colx : ax;
    height = rgb ? coly : ay;
    if(!(pixels = malloc(width * height * 4))) {
        goto ret;
    }
    /* combine color & alpha into a single RGBA image */
    for(i=0; i<width * height; i++) {
        if(rgb) {
            pixels[i * 4] = rgb[i * 3];
            pixels[i * 4 + 1] = rgb[i * 3 + 1];
            pixels[i * 4 + 2] = rgb[i * 3 + 2];
        } else {
            *(unsigned int*)(pixels + i * 4) = 0xffffffff;
        }
        pixels[i * 4 + 3] = alpha ? alpha[i * 3] : 255;
    }
    /* create OpenGL texture */
    glGenTextures(1, &tex_id);
    glBindTexture(GL_TEXTURE_2D, tex_id);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

ret:
    free(pixels);
    free(rgb);
    free(alpha);
    return tex_id;
}

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;
}

void reshape(int x, int y)
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0, (float)x / (float)y, 1.0, 1000.0);
    glViewport(0, 0, x, y);
}
eikona3

Επίλογος

Κάπου εδώ τελειώνει αυτή η πρώτη σειρά άρθρων για την εισαγωγή στον προγραμματισμό 3D γραφικών με το OpenGL. Σε αυτά τα 4 άρθρα καλύψαμε όλα τα βασικά, ξεκινώντας απο το πώς δουλεύει το rendering pipeline, μιλήσαμε για όλες τις βασικές τεχνικές, zbuffering, φωτισμό, texture mapping, και alpha blending.

Αυτά που καλύψμε είναι μια καλή αρχή για όποιον θέλει να ασχοληθέι με προγραμματισμό γραφικών, αλλα δεν είναι παρα η επιφάνεια του τομέα που λέγεται γραφικά. Αν και η εισαγωγική αυτή σειρά τελειώνει εδώ, δέν αποκλείεται σε κάποιο μελοντικό τεύχος του περιοδικού να ρίξουμε μια ματιά και βαθύτερα, στα πιό προχωριμένα features του OpenGL όπως οι GLSL shaders, ή ακόμα να δούμε και non-realtime αλγορίθμους γραφικών όπως το ray tracing.

Μέχρι τότε σας χαιρετώ, και περιμένω τα σχόλια ή απορείες σας πάνω σε αυτά τα άρθρα ή και σε οτιδίποτε άλλο έχει να κάνει με προγραμματισμό 3D γραφικών στο email μου: nuclear@mutantstargoat.com