Μέχρι τώρα έχουμε δει πως η GPU διαχειρίζεται μεγάλο πλήθος από παράλληλα threads (νήματα), όπου το καθένα είναι υπεύθυνο να αναθέσει σε κάθε τμήμα της τελικής εικόνας το χρώμα του. Αν και κάθε παράλληλο thread είναι τυφλό προς τα υπόλοιπα, χρειάζεται να μπορούμε να στείλουμε κάποιες εισόδους από τη CPU (ΚΜΕ) σε όλα τα threads. Εξ' αιτίας της αρχιτεκτονικής της κάρτας γραφικών, αυτές οι έισοδοι θα είναι ίδιες (uniform - ομοιόμορφες) για όλα τα threads, και κατ' ανάγκη είναι μόνο για ανάγνωση (read only). Με άλλα λόγια, κάθε thread παίρνει τα ίδια δεδομένα τα οποία μπορεί να διαβάσει αλλά όχι και να αλλάξει.
Αυτές οι είσοδοι λέγονται uniform
και υπάρχουν για τους περισσότερους από τους υποστηριζόμενους τύπους: float
, vec2
, vec3
, vec4
, mat2
, mat3
, mat4
, sampler2D
και samplerCube
. Τα uniforms ορίζονται μαζί με τον αντίστοιχο τύπο στην κορυφή του shader, αμέσως μετά την ανάθεση της ακρίβειας κινητής υποδιαστολής (default floating point precision).
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution; // Μέγεθος καμβά (πλάτος, ύψος)
uniform vec2 u_mouse; // θέση του mouse σε συντεταγμένες οθόνης (σε pixels)
uniform float u_time; // Χρόνος σε δευτερόλεπτα (seconds) από τη φόρτωση της σελίδας
Μπορούμε να φανταστούμε τα uniforms σαν μικρές γέφυρες ανάμεσα στην CPU και τη GPU. Τα ομόματα θα διαφέρουν από υλοποίηση σε υλοποίηση, αλλά σε αυτά τα παραδείγματα πάντα τα περνάω σαν: u_time
(χρόνος σε δευτερόλεπτα από τη στιγμή που ξεκίνησε ο shader), u_resolution
(μέγεθος του πίνακα όπου ζωγραφίζεται ο shader) και u_mouse
(θέση του mouse μέσα στον πίνακα σε pixels). Ακολουθώ τη σύμβαση να προσθέτω u_
πριν από το όνομα του uniform ώστε να είμαι σαφής ως προς τη φύση της μεταβλητής, αλλά θα συναντήσετε όλων των ειδών τα ονόματα για uniforms. Για παράδειγμα, το ShaderToy.com χρησιμοποιεί τα ίδια uniforms αλλά με τα ακόλουθα ονόματα:
uniform vec3 iResolution; // ανάλυση του viewport (περιοχή "θέασης") (σε pixels)
uniform vec4 iMouse; // συντεταγμένες του mouse σε pixel. xy: τρέχουσα θέση, zw: θέση click
uniform float iTime; // χρόνος εκτέλεσης (playback) του shader (σε seconds)
Αλλά αρκετά με τα λόγια, ας δούμε τα uniforms στην πράξη. Στον ακόλουθο κώδικα χρησιμοποιούμε το u_time
- τον αριθμό δευτερολέπτων από τη στιγμή που ο shaders άρχισε να εκτελείται - σε συνδυασμό με μια ημιτονοειδή συνάρτηση για να δώσουμε κίνηση στη μεταβολή του κόκκινου στον πίνακα.
Όπως βλέπετε, η GLSL έχει κι άλλες εκπλήξεις. Η GPU υποστηρίζει σε hardware συναρτήσεις γωνίας, τριγωνομετρικές και εκθετικές. Μερικές από αυτές τις συναρτήσεις είναι: sin()
, cos()
, tan()
, asin()
, acos()
, atan()
, pow()
, exp()
, log()
, sqrt()
, abs()
, sign()
, floor()
, ceil()
, fract()
, mod()
, min()
, max()
και clamp()
.
Ώρα να παίξουμε πάλι με τον παραπάνω κώδικα.
-
Επιβραδύνετε τη συχνότητα μέχρι η αλλαγή χρώματος σχεδόν να μη γίνεται αντιληπτή.
-
Επιταχύνετέ τη μέχρι να βλέπετε ένα ενιαίο χρώμα που δε θα τρεμοσβήνει (flicker).
-
Παίξτε με τα τρία κανάλια (RGB) σε διαφορετικές συχνότητες για να πάρετε ενδιαφέροντες συνδυασμούς και συμπεριφορές.
Κατά τον ίδιο τρόπο που η GLSL μας δίνει μια προκαθορισμένη (default) έξοδο, vec4 gl_FragColor
, μας δίνει και μια προκαθορισμένη είσοδο, vec4 gl_FragCoord
, η οποία περιέχει τις συντεταγμένες οθόνης του pixel ή τεμάχιου οθόνης - screen fragment στο οποίο δουλεύει το ενεργό thread. Με το vec4 gl_FragCoord
, ξέρουμε που δουλεύει ένα thread μέσα στον πίνακα. Σε αυτή την περίπτωση δεν το ονομάζουμε uniform
γιατί θα είναι διαφορετικό από το ένα thread στο άλλο, αντίθετα, το gl_FragCoord
λέγεται varying.
Στον παραπάνω κώδικα κανονικοποιούμε (normalize) τις συντεταγμένες του fragment (τεμαχίου) διαιρώντας το με τη συνολική ανάλυση του πίνακα. Με αυτό τον τρόπο, οι τιμές θα βρίσκονται μεταξύ 0.0
και 1.0
, κάτι που το κάνει εύκολο να αντιστοιχίσουμε τις τιμές X και Y στα κανάλια RED (κόκκινο) και GREEN (πράσινο).
Στον κόσμο των shaders, δεν έχουμε και πολλές δυνατότητες για debugging (διόρθωση σφαλμάτων) πέρα από το να αναθέτουμε έντονα χρώματα σε μεταβλητές και να προσπαθούμε να βγάλουμε άκρη από αυτά. Θα ανακαλύψετε πως κάποιες φορές το να προγραμματίζετε σε GLSL είναι πολύ παρόμοιο με το να φτιάχνει κανείς καραβάκια μέσα σε μπουκάλια. Είναι εξ' ίσου δύσκολο, όμορφο και ευχάριστο.
Ώρα να δοκιμάσουμε να τσεκάρουμε πόσο έχουμε κατανοήσει αυτό τον κώδικα.
-
Μπορείτε να βρείτε που στον καμβά μας βρίσκονται οι συντεταγμένες
(0.0, 0.0)
; -
Ή οι συντεταγμένες
(1.0, 0.0)
,(0.0, 1.0)
,(0.5, 0.5)
και(1.0, 1.0)
; -
Μπορείτε να βρείτε πως να χρησιμοποιήσετε το
u_mouse
γνωρίζοντας πως οι τιμές είναι σε pixels και ΟΧΙ κανονικοποιημένες; Μπορείτε να το χρησιμοποιήσετε για να κινήστε τα χρώματα; -
Μπορείτε να επινοήσετε έναν ενδιαφέροντα τρόπο να αλλάζετε το χρωματικό συνδυασμό χρησιμοποιώντας τις συντεταγμένες
u_time
καιu_mouse
?
Αφού ολοκληρώσετε αυτές τις ασκήσεις, θα αναρωτηθείτε ίσως πού αλλού μπορείτε να χρησιμοποιήστε τις νέες σας shaderοδυνάμεις. Στο επόμενο κεφάλαιο θα δούμε πως να φτιάξετε τα δικά σας εργαλεία shaders σε three.js, Processing, και openFrameworks.