Κεφάλαιο 2: OpenTK Classes

Todo:

2. OpenTK Classes
2.1 OpenTK.Input
2.2 OpenTK.Math
2.3 Rendering Context
2.4 OpenTK.Fonts & Timing
2.5 OpenTK.OpenGL
2.6 OpenTK.OpenAL [75%]

OpenGL

This page is the Greek Translation of OpenGL

Αυτό το μέρος του εγχειριδίου θα εισάγει το χρήστη στο προγραμματισμό γραφικών με την OpenGL.

Θα καλυφθούν σημαντικά ζητήματα όπως υφές, σκιές κτλ καθώς και το πώς χρησιμοποιούνται οι OpenTK.Utility κλάσεις για να βοηθήσουν σε κάποιες απλές εργασίες.

  • window-related, Viewing, etc. (ένα είδος Quickstart Template)
  • Καταστάσεις
  • Υφές
  • Γεωμετρία
  • Σκιάσεις

Γεωμετρία

This page is the Greek translation of Geometry

Εδώ θα συζητήσουμε το πώς ορίζουμε ή σχεδιάζουμε γεωμετρικά αντικείμενα χρησιμοποιώντας την OpenTK.OpenGL.

Θα επικεντρωθούμε στην αποθήκευση της γεωμετρίας κατευθείαν στη Video Memory χρησιμοποιώντας Vertex Buffer Objects (VBO). Αυτό είναι, όπως φαίνεται και από το όνομά του, όταν θέλουμε να αποθηκεύουμε στατικά αντικείμενα από το Περιβάλλον.

[υπό κατασκευή]

* 6. Drawing Optimizations

1. Η κορυφή

This page is the Greek translation of 1. The vertex

To Vertex (κορυφή) (πληθ. Vertices) ορίζει ένα σύνολο Ιδιοτήτων που σχετίζονται με ένα μοναδικό σημείο στο χώρο. Σε ένα στατικό περιβάλλον η κορυφή συνήθως περιλαμβάνει συντεταγμένες Θέσης, Κάθετου Διανύσματος, Χρώματος και/ή Υφής. Η μόνη ιδιότητα που πρέπει υποχρεωτικά να προσδιοριστεί είναι η θέση της κορυφής, η οποία συνήθως αποτελείται από τρεις συντεταγμένες τύπου float. Στην αναπαράσταση με βάση τη σκίαση είναι επίσης δυνατό να ορίσουμε κάποιες ιδιότητες για κορυφές οι οποίες ήταν άγνωστες στην OpenGL, όπως Ακτινοβολία ή Δείκτης οστών και βάρος για κίνηση με σκελετό. Για απλότητα θα ξαναφτιάξουμε ένα από τα formats των κορυφών που η OpenGL γνωρίζει ήδη, το λεγόμενο InterleavedArrayFormat.T2fN3fV3f. Το format αυτό περιέχει 2 αριθμούς τύπου float για συνιστώσες υφής, 3 floats για το κάθετο διάνυσμα και άλλους 3 για το προσδιορισμό της θέσης.
Χάρη στη βιβλιοθήκη μαθηματικών (Math-Library) που περιέχεται στην OpenTK, μπορούμε να προσδιορίσουμε μία βοηθητική Vertex δομή η οποία είναι πολύ πιο απλή και εύχρηστη από ένα πίνακα τύπου float[]. Αποτελείται συνολικά από 8 float, ή 32 byte:

[StructLayout(LayoutKind.Sequential)]
struct Vertex
{ // mimic InterleavedArrayFormat.T2fN3fV3f
  public Vector2 TexCoord;
  public Vector3 Normal;
  public Vector3 Position;
}

Μπορούμε τώρα να ορίσουμε ένα πίνακα από κορυφές για να προσδιορίσουμε πολλαπλά σημεία και να έχουμε εύκολη πρόσβαση/αναφορά σε αυτά:

Vertex[] Vertices;

Ο πίνακας κορυφών Vertices μπορεί τώρα να συμπληρωθεί με δεδομένα. Η προσπέλαση των δεδομένων αυτών είναι τόσο απλή όσο και στο επόμενο παράδειγμα:

Vertices = new Vertex[ n ]; // -1 < i < n  (Remember that arrays start at Index 0 and end at Index n-1.)
 
// examples how to assign values to the Vector's components:
Vertices[ i ].Position = new Vector3( 2f, -3f, .4f ); // create a new Vector and copy it to Position.
Vertices[ i ].Normal = Vector3.UnitX; // this will copy Vector3.UnitX into the Normal Vector.
Vertices[ i ].TexCoord.X = 0.5f; // the Vectors are structs, so the new keyword is not required.
Vertices[ i ].TexCoord.Y = 1f;
 
// Ofcourse this also works the other way around, using the Vectors as the source.
Vector2 UV = Vertices[ i ].TexCoord;

Ένας Index είναι απλά ένα byte, ushort ή uint, σαν δείκτης στον πίνακα Vertices. Άρα, αν αποφασίσουμε να σχεδιάσουμε την ίδια κορυφή 100 φορές αντί να το αποθηκεύουμε 100 φορές στον πίνακα Vertices είναι προτιμότερο να το προσπελαύνουμε 100 φορές από τον πίνακα Indices:

uint[] Indices;

Βασικά ο πίνακας Indices χρησιμοποιείται για να δηλώνουμε τα κύρια σημεία και ο πίνακας Vertex για να δηλώνουμε τα γωνιακά.

Επίσης μπορούμε να χρησιμοποιήσουμε συλλογές (collections) για να αποθηκεύουμε τα Vertices, αλλά είναι καλύτερα να κρατήσετε ένα απλό πίνακα για να είστε σίγουροι ότι τα Indices είναι έγκυρα ανά πάσα στιγμή.

Οι πίνακες Vertices και Indices μπορούν τώρα να χρησιμοποιηθούν για να περιγράψουν τις κορυφές ενός οποιουδήποτε Γεωμετρικού Πρωτεύοντος Τύπου.

Μόλις οι πίνακες γεμίσουν με δεδομένα μπορούν να σχεδιαστούν σαν ένας Immediate ModeVertex πίνακας ή να σταλούν σε ένα Vertex Buffer Object.

Δημιουργία μιας Windows.Forms+GLControl εφαρμογής

This page is the Greek Translation of Building a Windows.Forms+GLControl based application

Αυτό το εγχειρίδιο προϋποθέτει κάποια οικειότητα με την εφαρμογή Windows.Forms σε Visual Studio 2005/C#, και τουλάχιστον βασική γνώση της OpenGL.

Για αρχή, είναι αρκετά διαφορετική η προσέγγιση που πρέπει να έχει κάποιος όταν σχεδιάζει ένα παιχνίδι/εφαρμογή χρησιμοποιώντας το GLControl σε μια Windows.Form, απ’ ότι όταν χρησιμοποιεί το GameWindow. Το GLControl είναι πιο χαμηλού επιπέδου επομένως πρέπει να κάνετε πολλά πράγματα μόνοι σας, όπως για παράδειγμα τις μετρήσεις χρόνου. Στο GameWindow έχετε περισσότερες ευκολίες!

Αν έρχεστε από ένα "main-loop-background" (C/SDL/Allegro etc.) θα πρέπει να σκεφτείτε τα πάντα από την αρχή στο προγραμματισμό παιχνιδιών. Θα πρέπει να αλλάξετε τρόπο σκέψης με κάτι σαν: «Σε ποιο event πρέπει να κολλήσω, και σε ποια να δώσω trigger, και πότε;».

Γιατί λοιπόν να χρησιμοποιήσετε Windows.Forms+GLControl αντί για GameWindow?

Το πρώτο πράγμα για το οποίο πρέπει να αποφασίσετε είναι το εξής:
"Πράγματι χρειάζομαι την επιπρόσθετη πολυπλοκότητα των Windows.Forms και του embedded GLControl αντί για ένα απλό GameWindow?"

Να μερικοί λόγοι γιατί πιθανόν να θέλατε αυτή την επιπρόσθετη πολυπλοκότητα:

  1. Θέλετε να φτιάξετε ένα GUI-rich/RAD είδος εφαρμογής χρησιμοποιώντας Windows.Forms controls πχ level editors model viewers/editors μπορεί να ανήκουν σε αυτή τη κατηγορία ενώ ένα windowed παιχνίδι είναι πιο κοντά σε μία GameWindow εφαρμογή.
  2. Θέλετε να έχετε ένα ενσωματομένο OpenGL rendering panel μέσα σε μία ήδη υπάρχουσα Windows.Forms εφαρμογή.
  3. Θέλετε να έχετε τη δυνατότητα να κάνετε drag-and-drop στο rendering panel, για παράδειγμα να τοποθετήσετε ένα μοντέλο αρχείου σε ένα μοντέλο viewer.

Υποθέτοντας λοιπόν ότι έχετε τουλάχιστον ένα από αυτούς τους λόγους για να θέλετε να φτιάξετε μία Windows.Forms+GLControl – βασισμένη εφαρμογή, να τα βήματα για να τη φτιάξετε:

Προσθέτοντας το GLControl στη Windows.Form σας

Υποθέτω ότι χρησιμοποιείτε Visual Studio 2005 Express Edition. Η διαδικασία μπορεί να διαφέρει αν χρησιμοποιείτε VS2008 or Monodevelop – δε ξέρω τις λεπτομέρειες για αυτά – αλλά οι επόμενες ενότητες θα πρέπει να είναι οι ίδιες με όποιο τρόπο και να προσθέσετε το GLControl.

Για αρχή, δημιουργήστε μία Form στην οποία θα τοποθετήσετε το GLControl σας. Κάντε δεξί κλικ σε κάποιο άδειο σημείο του Toolbox, διαλέξτε "Choose Items..." και ψάξτε το OpenTK.dll. Σιγουρευτείτε ότι μπορείτε να βρείτε το "GLControl" στη λίστα ".NET Framework Components", όπως στην παρακάτω εικόνα:

Στη συνέχεια μπορείτε να προσθέσετε το GLContrοl στη φόρμα σας σαν οποιοδήποτε .ΝΕΤ control και ένα GLControl με όνομα

glControl1<code> θα προστεθεί στη <code>Form<code> σας.
 
Το πρώτο πράγμα που θα παρατηρήσετε είναι τα άχρηστα γραφικά μέσα στο <code>glControl1

. Βασικά, το GLControl χρησιμοποιεί ένα GLContext, όπως λέγεται για να κάνει το πραγματικό GL-rendering και ούτω καθ’ εξής, και αυτό το περιεχόμενο δημιουργείται μόνο στην ώρα εκτέλεσης, όχι σχεδίασης. Οπότε μην ανησυχείτε.

Σειρά δημιουργίας

Είναι σημαντικό να το θυμάστε ότι το GLContext του glControl1 δημιουργείται κατά την εκτέλεση, εφόσον δε μπορείτε να έχετε πρόσβαση ή να αλλάξετε τις ιδιότητες του glControl1 μέχρι να δημιουργηθεί το GLCοntext. Το ίδιο ισχύει και για οποιαδήποτε GL.* εντολή (ή Glu για αυτό το θέμα!). Η σειρά σύλληψης είναι η εξής:

  1. Πρώτα τρέχει ο Windows.Form constructor. Μην πειράζετε το glControl/GL.
  2. Στη συνέχεια γίνεται trigger στο Load event της φόρμας. Μπορείτε να πειράξετε το glControl/GL.
  3. Αφού τρέξει ο event handler, οποιοσδήποτε event handler μπορεί να επηρεάσει το glControl/GL.

Επομένως μία προσέγγιση για να λύσετε αυτό το πρόβλημα είναι να έχετε μία λογική μεταβλητή bool loaded = false, μέλος στη φόρμα σας, η οποία θα γίνει true κατά το Load του event handler:

   using OpenTK.OpenGL;
  using OpenTK.OpenGL.Enums;
 
  public partial class Form1 : Form
  {
    bool loaded = false;
 
    public Form1()
    {
      InitializeComponent();
    }
 
    private void Form1_Load(object sender, EventArgs e)
    {
      loaded = true;
    }
  }

Κατόπιν σε οποιονδήποτε event handler θα έχει πρόσβαση το glControl1/GL μπορείτε να προσθέσετε τον εξής έλεγχο:

     private void glControl1_Resize(object sender, EventArgs e)
    {
      if (!loaded)
        return;
    }

Hello World!

Ο απλούστερος κώδικας που μπορείτε να προσθέσετε σε αυτό το στάδιο για να δείτε κάποιο αποτέλεσμα είναι να προσθέσετε έναν event handler στο Paint event του glControl1 και σε αυτό να γράψετε τα εξής:

     private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      if (!loaded) // Play nice
        return;
 
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
      glControl1.SwapBuffers();
    }

Ναι! Ένα μαύρο παράθυρο! Παρατηρήστε ότι το GLControl παρέχει ένα χρώμα - και ένα buffer που πρέπει να καθαρίζουμε χρησιμοποιώντας GL.Clear(). [TODO: Πώς γίνεται να ελέγχουμε ποιους buffers και formats έχει το GLControl; Είναι δυνατόν;]

Στη συνέχεια θα μπορούσαμε να ρυθμίσουμε το καθαρό χρώμα. Το κατάλληλο μέρος για να κάνουμε την αρχικοποίηση GL είναι στον Load event handler της φόρμας:

     private void Form1_Load(object sender, EventArgs e)
    {
      loaded = true;
      GL.ClearColor(Color.SkyBlue); // Yey! .NET Colors can be used directly!
    }

Αρχικοποίηση του Viewport

Το επόμενο πράγμα που μπορούμε να κάνουμε είναι να σχεδιάσουμε ένα κίτρινο τρίγωνο.

Πρώτα χρειάζεται να είστε καλός γνώστης της OpenGL και να μπορείτε να στήσετε ένα ορθογώνιο πίνακα προβολής χρησιμοποιώντας τη GL.Ortho(). Χρειάζεται επίσης να καλέσετε τη GL.Viewport().

Προς το παρόν θα προσθέσουμε αυτό στον Load event handler από τον άλλο κώδικα αρχικοποίησης – αγνοώντας το γεγονός ότι μπορεί να θέλουμε να επιτρέψουμε στο χρήστη να αλλάζει το μέγεθος του παραθύρου/GLContrl. Θα ασχοληθούμε με το θέμα αλλαγής μεγέθους παραθύρου αργότερα.

Εδώ έβαλα τη viewport initialization σε μία ξεχωριστή μέθοδο για την κάνω λίγο πιο ευανάγνωστη.

     private void Form1_Load(object sender, EventArgs e)
    {
      loaded = true;
      GL.ClearColor(Color.SkyBlue);
      SetupViewport();
    }
 
    private void SetupViewport()
    {
      int w = glControl1.Width;
      int h = glControl1.Height;
      GL.MatrixMode(MatrixMode.Projection);
      GL.LoadIdentity();
      GL.Ortho(0, w, 0, h, -1, 1); // Bottom-left corner pixel has coordinate (0, 0)
      GL.Viewport(0, 0, w, h); // Use all of the glControl painting area
    }

Kαι μεταξύ των Clear() και SwapBuffers() το κίτρινο τρίγωνό μας:

     private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      if (!loaded)
        return;
 
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
 
      GL.MatrixMode(MatrixMode.Modelview);
      GL.LoadIdentity();
      GL.Color3(Color.Yellow);
      GL.Begin(BeginMode.Triangles);
      GL.Vertex2(10, 20);
      GL.Vertex2(100, 20);
      GL.Vertex2(100, 50);
      GL.End();
 
      glControl1.SwapBuffers();
    }

Voila!

Είσοδος από το πληκτρολόγιο

Το επόμενο πράγμα που θέλουμε είναι η δυνατότητα στο χρήστη να κινεί το τρίγωνο. Κάθε φορά που πιέζει το Space, θέλουμε το τρίγωνο να κινείται κατά ένα pixel δεξιά.

Οι δύο γενικές προσεγγίσεις στην είσοδο από πληκτρολόγιο σε ένα GLControl σενάριο είναι να χρησιμοποιούμε τα Windows.Forms key events και την OpenTK KeyboardDevice. Εφόσον το υπόλοιπο από το πρόγραμμά μας έγκειται στον κόσμο του Windows.Forms (το παράθυρό μας μπορεί να είναι ένα μικρό κομμάτι ενός μεγαλύτερου GUI), θα παίξουμε ωραία με τα Windows.Forms key events σε αυτό τον οδηγό.

Θα έχουμε μια μεταβλητή int x=0; την οποία θα μειώνουμε με έναν KeyDown event handler. Αν αυτά τα προσθέσουμε στο glControl1 και όχι στη Form, σημαίνει ότι πρέπει να εστιάσουμε στο glControl, για παράδειγμα ο χρήστης πρέπει να κάνει κλικ για να σταλθούν τα key events στον handler μας.

     int x = 0;
    private void glControl1_KeyDown(object sender, KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Space)
        x++;
    }

Προσθέτουμε την εντολή GL.Translate() στον Paint event handler:

     private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      if (!loaded)
        return;
 
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
 
      GL.MatrixMode(MatrixMode.Modelview);
      GL.LoadIdentity();
 
      GL.Translate(x, 0, 0); // position triangle according to our x variable
 
      ...
    }

...και τρέχουμε το πρόγραμμά μας. Αλλά μισό λεπτό! Τίποτα δε συμβαίνει όταν πιέζουμε το Space! Ο λόγος είναι ότι το glControl1 δε χρωματίζεται όλη την ώρα, ο window manager των λειτουργικών συστημάτων (Windows/X/OSX) προσπαθεί να εκτελέσει όσα λιγότερα Paint events γίνεται. Μόνο στην αλλαγή μεγέθους και σε λίγες ακόμα καταστάσεις ενεργοποιούνται τα Paint events.

Επομένως, αυτό που θα θέλαμε είναι να είχαμε ένα τρόπο να πούμε στον window manager «Αυτό το control χρειάζεται να χρωματίζεται όταν αλλάζουν τα δεδομένα στα οποία βασίζεται». Θέλουμε δηλαδή να ειδοποιούμε τον window manager ότι το glControl1 πρέπει να επαναχρωματιστεί. Εύκολο, με την εντολή Invalidate():

     private void glControl1_KeyDown(object sender, KeyEventArgs e)
    {
      if (!loaded)
        return;
      if (e.KeyCode == Keys.Space)
      {
        x++;
        glControl1.Invalidate();
      }
    }

Συμπεριφορά του Focus

Αν μου μοιάζετε λίγο θα αναρωτιέστε τώρα πώς δουλεύει αυτό το focus, ας μάθουμε!
Ένας απλός τρόπος είναι να χρωματίσετε το τρίγωνο κίτρινο όταν το glControl1 είναι σε focus και μπλε όταν δεν είναι. Κάτι τετοιο δηλαδή:

     private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      ...
 
      if (glControl1.Focused) // Simple enough :)
        GL.Color3(Color.Yellow);
      else
        GL.Color3(Color.Blue);
      GL.Begin(BeginMode.Triangles);
 
      ...
    }

Οπότε όποια στιγμή το τρίγωνο είναι κίτρινο, το πλήκτρο Space πρέπει να δουλεύει, και όταν είναι μπλε όποιο πλήκτρο και να πατηθεί θα πρέπει να αγνοείται.

Επιτέλους Ελευθερία: Αλλάζοντας το μέγεθος του παραθύρου

Τώρα θα ασχοληθούμε με ένα πιο σοβαρό ζήτημα: την αλλαγή μεγέθους των παραθύρων.

Όποια στιγμή ένα Windows.Forms control αλλάζει μέγεθος γίνεται trigger στο Resize event. Αυτό ισχύει και για το glControl1. Ένα το κρατούμενο.

Το επόμενο βήμα είναι να βρούμε "Τι χρειάζεται να κάνουμε για να ενημερώνουμε όταν ένα GLControl αλλάζει μέγεθος;" και η απάντηση είναι "Το viewport και τον πίνακα προβολών"

Απ' ότι φαίνεται ότι θα χρησιμοποιήσουμε το SetupViewport() ακόμα μία φορά! Προσθέστε έναν event handler στο Resize event of glControl1 και συμπληρώστε τον:

     private void glControl1_Resize(object sender, EventArgs e)
    {
      SetupViewport();
    }

Όμως υπάρχει ακόμα ένα πρόβλημα: αν μειώσετε το μέγεθος του παραθύρου μετακινώντας για παράδειγμα το κάτω-αριστερά άκρο του, δε θα γίνει αυτόματα trigger στον επαναχρωματισμό. Αυτό γίνεται γιατί ο window manager υποθέτει που βρίσκεται το (0, 0) pixel του control, ονομαστικά στην επάνω αριστερά γωνία του control. (δοκιμάστε να αλλάξετε μέγεθος μετακινώντας την πάνω αριστερά γωνία – το τρίγωνο αλλάζει συνεχώς χρώμα!) Αυτό που μπορούμε να κάνουμε για να το διορθώσουμε αυτό είναι να δώσουμε οδηγίες στον window manager ότι θέλουμε να γίνεται ο επαναχρωματισμός με κάθε event αλλαγής μεγέθους:

     private void glControl1_Resize(object sender, EventArgs e)
    {
      SetupViewport();
      glControl1.Invalidate();
    }

Θέλω τον κυρίως βρόχο μου: κίνηση με χρήση της Application.Idle

Τι θα συνέβαινε αν θέλαμε το τρίγωνό μας να περιστρεφόταν συνεχώς; Αυτό θα ήταν παιχνιδάκι σε ένα σενάριο με main loop: απλά θα αυξάναμε μία μεταβλητή rotation στην main loop, πριν σχεδιαστεί το τρίγωνο.
Όμως δεν έχουμε κανένα βρόχο, μόνο events!

Για να επιδιορθώσουμε αυτή την έλλειψη συνέχειας πρέπει να αναγκάσουμε τα Windows.Forms να το κάνουν με τον τρόπο που θέλουμε, δηλαδή να γίνεται trigger σε ένα event κάθε τρεις και λίγο, αρκετά συχνά όμως για να έχουμε την αίσθηση ότι γίνεται σε realtime.

Υπάρχουν αρκετοί τρόποι για να το πετύχουμε αυτό. Ένας είναι να προσθέσουμε ένα Timer control στη φόρμα μας, αλλάζοντας τη rotation στο Tick του event handler. Ένας άλλος είναι να προσθέσουμε και ένα Thread στο παιχνίδι. Ο πρώτος είναι πολύ υψηλού επιπέδου και αργός ενώ ο δεύτερος πολύ χαμηλού επιπέδου και λίγο πιο δύσκολο να δουλέψει σωστά.

Οπότε θα ακολουθήσουμε ένα τρίτο δρόμο και θα χρησιμοποιήσουμε ένα Windows.Forms event σχεδιασμένο να εκτελείται ειδικά για την περίπτωση όπου «τίποτ’ άλλο δε συμβαίνει», δηλαδή το Application.Idle event.
Όπως μπορεί ήδη να μαντέψατε το event αυτό είναι ιδιαίτερο με πολλούς τρόπους. Δε σχετίζεται με καμία φόρμα ή άλλο control, αλλά με το πρόγραμμα σαν σύνολο. Δε μπορείτε να το προσθέσετε από το GUI Designer, αλλά με το χέρι – για παράδειγμα, στο Load event:

     private void Form1_Load(object sender, EventArgs e)
    {
      loaded = true;
      GL.ClearColor(Color.SkyBlue);
      SetupViewport();
      Application.Idle += new EventHandler(Application_Idle); // press TAB twice after +=
    }
 
    void Application_Idle(object sender, EventArgs e)
    {
    }

Ένα καλό με το Idle event είναι ότι οι αντίστοιχοι event handlers εκτελούνται στο the Windows.Forms thread. Αυτό είναι καλό γιατί σημαίνει ότι μπορούμε να έχουμε πρόσβαση σε όλα τα GUI controls χωρίς να χρειάζεται να ανησυχούμε για θέματα των threads, ένας μπελάς που θα είχαμε να αντιμετωπίσουμε αν φτιάχναμε δικό μας thread.
Επομένως απλά αυξάνουμε τη rotation μεταβλητή στον Idle event handler και Invalidate() στο glControl1 – κατά τα γνωστά.

     float rotation = 0;
    void Application_Idle(object sender, EventArgs e)
    {
      // no guard needed -- we hooked into the event in Load handler
      rotation += 1;
      glControl1.Invalidate();
    }

Ας ενημερώσουμε και τον άλλο κώδικά μας:

     private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      ...
      if (glControl1.Focused)
        GL.Color3(Color.Yellow);
      else
        GL.Color3(Color.Blue);
      GL.Rotate(rotation, Vector3.UnitZ); // OpenTK has this nice Vector3 class!
      GL.Begin(BeginMode.Triangles);
      ...
    }

Απολαύστε το!

Το τρίγωνο περιστρέφεται πιο αργά όταν το παράθυρο είναι μεγάλο! Πώς γίνεται;

Αυτό μπορεί και να μην ισχύει αν έχετε ένα πολύ γρήγορο υπολογιστή με μία πολύ γρήγορη κάρτα γραφικών, αλλά θέλετε το παιχνίδι σας να τρέχει και σε άλλους υπολογιστές, έτσι δεν είναι;

Ο λόγος είναι ότι γενικά η απεικόνιση 3d γραφικών σε παράθυρο είναι πολύ πιο αργή απ’ ότι σε πλήρη οθόνη.

Αλλά μπορείτε να μειώσετε το κακό χρησιμοποιώντας μία τεχνική που λέγεται
frame-rate independent animation. Η ιδέα είναι απλή: η αύξηση της μεταβλητής rotation όχι κατά 1 αλλά κατά μία ποσότητα που εξαρτάται από τη τρέχουσα ταχύτητα απεικόνισης (αν η ταχύτητα είναι αργή, αύξηση της rotation κατά μία μεγαλύτερη ποσότητα απ’ όταν η ταχύτητα είναι μεγάλη.

Αλλά πρέπει να μπορείτε να μετρήσετε τη τρέχουσα ταχύτητα απεικόνισης, ή ισοδύναμα, την ώρα που απαιτείται για να απεικονιστεί ένα frame.

Από τη .NET2.0 υπάρχει μία κλάση διαθέσιμη για να κάνουμε μετρήσεις υψηλής ακρίβειας που λέγεται Stopwatch. Να πώς μπορείτε να τη χρησιμοποιήσετε:

     Stopwatch sw = new Stopwatch();
    sw.Start();
    MyAdvancedAlgorithm();
    sw.Stop();
    double milliseconds = sw.Elapsed.TotalMilliseconds;

(μη το δοκιμάσετε με την DateTime.Now -- έχει σφάλμα 10 ή και περισσότερα milliseconds, δηλαδή ίδιας τάξης μεγέθους με μία τυπική frame rendering – μάταιος κόπος..)

Τώρα,αν θα μπορούσαμε να μετρήσουμε το χρόνο που χρειαζόμαστε για να κάνουμε τον glControl χρωματισμό, θα ήταν ένα πρώτο βήμα για να φτιάξουμε ένα είδος κίνησης ανεξάρτητης από το frame-rate. Αλλά υπάρχει ένας ακόμα πιο κομψός τρόπος: μπορούμε να μετρήσουμε όλο το χρόνο που δεν είναι Application.Idle χρόνος! Έτσι θα είμαστε σίγουροι ότι δε μετράμε μόνο το χρωματισμό, αλλά ό,τι συμβαίνει από το τελευταίο Idle τρέξιμο:

     Stopwatch sw = new Stopwatch(); // available to all event handlers
    private void Form1_Load(object sender, EventArgs e)
    {
      ...
      sw.Start(); // start at application boot
    }
 
    float rotation = 0;
    void Application_Idle(object sender, EventArgs e)
    {
      // no guard needed -- we hooked into the event in Load handler
 
      sw.Stop(); // we've measured everything since last Idle run
      double milliseconds = sw.Elapsed.TotalMilliseconds;
      sw.Reset(); // reset stopwatch
      sw.Start(); // restart stopwatch
 
      // increase rotation by an amount proportional to the
      // total time since last Idle run
      float deltaRotation = (float)milliseconds / 20.0f;
      rotation += deltaRotation;
 
      glControl1.Invalidate();
    }

Τέλεια! Το τρίγωνο περιστρέφεται με την ίδια ταχύτητα ανεξάρτητα από το μέγεθος του παραθύρου.

Θέλω ένα FPS μετρητή!

Ναι, κι εγώ. Είναι αρκετά απλό τώρα που έχουμε ένα Stopwatch.

Η ιδέα είναι απλά να μετράμε τα Idle τρεξίματα, και κάθε δευτερόλεπτο περίπου να ενημερώνουμε ένα Label control με τον μετρητή! Αλλά θα πρέπει να ξέρουμε πότε πέρασε το ένα δευτερόλεπτο, οπότε χρειαζόμαστε μία ακόμα μεταβληρή αθροιστική, που να προσθέτει όλες τις χρονικές περιόδους μαζί.

Έχουν μαζευτεί πολλά στον Idle event handler, οπότε θα τον χωρίσω λίγο:

     void Application_Idle(object sender, EventArgs e)
    {
      double milliseconds = ComputeTimeSlice();
      Accumulate(milliseconds);
      Animate(milliseconds);
    }
 
    float rotation = 0;
    private void Animate(double milliseconds)
    {
      float deltaRotation = (float)milliseconds / 20.0f;
      rotation += deltaRotation;
      glControl1.Invalidate();
    }
 
    double accumulator = 0;
    int idleCounter = 0;
    private void Accumulate(double milliseconds)
    {
      idleCounter++;
      accumulator += milliseconds;
      if (accumulator > 1000)
      {
        label1.Text = idleCounter.ToString();
        accumulator -= 1000;
        idleCounter = 0; // don't forget to reset the counter!
      }
    }

Ο FPS μετρητής μας σε όλη του τη δόξα:

Δε μπορώ να μαζέψω τον ολοκληρωμένο κώδικα κάπου;

Φυσικά, να τος:

 using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using OpenTK.OpenGL;
using OpenTK.OpenGL.Enums;
using OpenTK.Math;
using System.Diagnostics;
 
namespace GLControlApp
{
  public partial class Form1 : Form
  {
    bool loaded = false;
 
    public Form1()
    {
      InitializeComponent();
    }
 
    Stopwatch sw = new Stopwatch(); // available to all event handlers
    private void Form1_Load(object sender, EventArgs e)
    {
      loaded = true;
      GL.ClearColor(Color.SkyBlue); // Yey! .NET Colors can be used directly!
      SetupViewport();
      Application.Idle += new EventHandler(Application_Idle); // press TAB twice after +=
      sw.Start(); // start at application boot
    }
 
    void Application_Idle(object sender, EventArgs e)
    {
      double milliseconds = ComputeTimeSlice();
      Accumulate(milliseconds);
      Animate(milliseconds);
    }
 
    float rotation = 0;
    private void Animate(double milliseconds)
    {
      float deltaRotation = (float)milliseconds / 20.0f;
      rotation += deltaRotation;
      glControl1.Invalidate();
    }
 
    double accumulator = 0;
    int idleCounter = 0;
    private void Accumulate(double milliseconds)
    {
      idleCounter++;
      accumulator += milliseconds;
      if (accumulator > 1000)
      {
        label1.Text = idleCounter.ToString();
        accumulator -= 1000;
        idleCounter = 0; // don't forget to reset the counter!
      }
    }
 
    private double ComputeTimeSlice()
    {
      sw.Stop();
      double timeslice = sw.Elapsed.TotalMilliseconds;
      sw.Reset();
      sw.Start();
      return timeslice;
    }
 
    private void SetupViewport()
    {
      int w = glControl1.Width;
      int h = glControl1.Height;
      GL.MatrixMode(MatrixMode.Projection);
      GL.LoadIdentity();
      GL.Ortho(0, w, 0, h, -1, 1); // Bottom-left corner pixel has coordinate (0, 0)
      GL.Viewport(0, 0, w, h); // Use all of the glControl painting area
    }
 
    private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      if (!loaded)
        return;
 
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
 
      GL.MatrixMode(MatrixMode.Modelview);
      GL.LoadIdentity();
 
      GL.Translate(x, 0, 0);
 
      if (glControl1.Focused)
        GL.Color3(Color.Yellow);
      else
        GL.Color3(Color.Blue);
      GL.Rotate(rotation, Vector3.UnitZ); // OpenTK has this nice Vector3 class!
      GL.Begin(BeginMode.Triangles);
      GL.Vertex2(10, 20);
      GL.Vertex2(100, 20);
      GL.Vertex2(100, 50);
      GL.End();
 
      glControl1.SwapBuffers();
    }
 
    int x = 0;
    private void glControl1_KeyDown(object sender, KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Space)
      {
        x++;
        glControl1.Invalidate();
      }
    }
 
    private void glControl1_Resize(object sender, EventArgs e)
    {
      SetupViewport();
      glControl1.Invalidate();
    }
  }
}