Kapitel 2: OpenTK Klassen

This page is the German Translation of Chapter 2: OpenTK Classes.

Zunächst einmal, was ist eigentlich OpenTK?

Einfachste Definition: OpenTK ist ein kostenfreies Projekt, dank dem man OpenGL, OpenGL|ES, OpenCL und OpenAL API`s in Managed-Programmiersprachen nutzen kann.

OpenTK wurde als Experementelle Spaltung vom Tao-Framework im Sommer 2006 ins Leben gerufen. Das Ursprüngliche Ziel war es, einen sauberen Wrapper als Tao.OpenGL zu schaffen, allerdings wurde ´später weitaus mehr in den Fokus genommen: Jetzt ist es möglich verschiedene Khronus und Creative API´s zu nutzen und es sorgt nun für die notwendigen Initialisierungen der Logik für jedes diese API´s. So ist OpenTK mit anderen API´s wie Tao, SlimDX, SDL oder GLFW am nächsten.

im Gegensatz zu den anderen Projekten, bietet OpenTK eine einheitliche Schnittstelle, welche für eine Überlegenheit in der verwalteten Laufzeit sorgt.
Statt Pointers verwendet OpenTK generics. Anstatt von Constants verwendet OpenTK Typenreine Enum´s. Statt Funktionslisten, trennt OpenTK pro Funktion die dazugehörige Kategory. Es ist eine allgemeine Mathematische Library integriert, die von den API´s direkt nutzbar ist.

Eigenschaften:

Mit OpenTK sind Spiele, Visualisierungstechniken und alle Sorten von Software realisierbar, die hohe Grafik, Audio und/oder Computing Ansprüche haben. Die License macht es möglich, Ihre Endanwendungen Kostenfrei und Kommerziell zu vertreiben.

Entwickeln einer Windows.Forms + GLControl basierten Anwendung

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

Dieses Tutorial geht davon aus, dass Sie bereits Erfahrung bei der Entwicklung von Windows.Forms Anwendungen mit Visual Studio 2005/C# haben und dass Sie ein Grundwissen über OpenGL besitzen. Außerdem wird davon ausgegangen, dass man den Artikel von oben nach unten durchliest, es ist ein Tutorial und keine Referenz.

Zum Anfang ist zu sagen, dass die Herangehensweise bei der Entwicklung einer Anwendung bzw. eines Spiel, welches GLControl in einer Windows.Form nutzt gänzlich anders ist im Vergleich zur Nutzung von GameWindow. GLControl arbeitet auf einer niedrigeren Erbene als GameWindow, sodass man z.B. die Zeitangaben selber zeichnen muss. Beim GameWindow bekommt man mehr Funktionen!

Wenn Sie bisher Spiele mittels einer Hauptschleife (C/SDL/Allegro etc.) programmiert haben, sollten Sie jetzt umdenken. Nun muss man sich stattdessen überlegen: Welche Ereignisse muss ich abfangen und welche Events muss ich wann auslösen?

Warum Windows.Forms+GLControl an Stelle von GameWindows nutzen?

Die erste Frage, die man sicht stellen muss, ist:
"Benötige ich wirklich die zusätzliche Komplexität von Windows.Forms + eingebettetes GLControl im Vergleich mit einem GameWindow in einem Fenster?"

Hier sind einige Gründe, die dafür sprechen:

  1. Sie wollen eine GUI/RAD-Anwendung entwickeln, die Windows.Forms Steuerelemente nutzt. Level Editoren oder Model-Viewer fallen in diese Kategorie während in einem Fenster laufende Spiele eher auf eine GameWindow-Anwendung hindeuten.
  2. Sie wollen ein OpenGL Rendering-Element in eine existierende Windows.Forms-Anwendung einbetten.
  3. Sie wollen eine Möglichkeit etwas in die OpenGL-Grafik per Drag & Drop hinein zu ziehen, z.B. eine existierende Model-Datei in einen Model-Viewer.

Wenn Sie mindestens einen Grund gefunden haben eine Windows.Forms+GLControl basierte Anwendung zu entwickeln, dann finden Sie nachfolgend die nächsten Schritte.

Das GLControl in ein Windows.Form integrieren
(An dieser Stelle wird davon ausgegangen, dass Sie Visual Studio 2005 Express Edition nutzen. Die Integration könnte bei Ihnen variieren, falls Sie Visual Studio 2008 oder MonoDevelop nutzen - aber die folgenden Sektionen sollten gleich sein, egal wie Sie das GLControl einfügen.)

Zu Beginn erzeugen Sie eine Form (Fenster), in das Sie später das GLControl platzieren werden. Klicken Sie mit der rechten Maustaste in den leeren Bereich der Toolbox, wählen Sie dann "Elemente auswählen ..." und suchen Sie nach OpenTK.dll. Stellen Sie sicher, dass "GLControl" wie im Bild unten abgebildet unter ".NET Framework-Komponenten" aufgelistet ist.

Nun platzieren Sie das GlControl in ihre Form so wie jedes andere .NET Steuerelement auch. Eine GLControl-Variable namens glControl1 wird jetzt zu Ihrer Form hinzugefügt.

Das erste was Sie bemerken werden, ist die komische Grafik innerhalb von glControl1. Unter der Haube nutzt GlControl einen sogenannten GLContext zum Rendern, der allerdings nur zur Laufzeit und noch nicht im Designer erzeugt wird. Kümmern Sie sich nicht drum.

Reihenfolge der Erzeugung
Den Fakt, dass der GLContext von glControl1 erst zur Laufzeit erzeugt wird, sollte man sich gut merken, denn die Eigenschaften von glControl1 können erst abgefragt oder geändert werden, wenn der GLContext erzeugt wurde. Das gleiche gilt für alle GL.*-Funktionen (oder Glu). Die Reihenfolge ist wie folgt:

  1. Der Konstruktor der Windows.Form wird ausgeführt. Zugriff auf glControl/GL nicht erlaubt
  2. Dann wird das Load-Ereignis der Form ausgelöst. Zugriff auf glControl/GL erlaubt
  3. Nachdem die Load-Ereignisbehandlung abgeschlossen ist, darf jeder Event Handler auf glControl/GL zugreifen.

Ein Ansatz zur Lösung des Problem ist es eine Instanzvariable bool loaded = false; in der Form zu haben, die im Load-Ereignis auf true gesetzt wird:

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

Nun muss man in jeden Ereignishandler, in dem man glControl1 nutzt, zuerst eine Sicherheitsprüfung einbauen:

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

Ein einfacher Weg ein Load-Ereignishandler zur Form hinzuzufügen, ist das Eigenschaften-Fenster zu nutzen:

  1. Auf die Titelleiste von Form1 im Designer klicken
  2. Sicherstellen die Eigenschaften von Form1 werden im Eigenschaften-Fenster angezeigt
  3. Auf den "Ereignisse"-Button klicken
  4. Doppelt auf die freie Zelle rechts vom Load-Handler klicken, um die Ereignisbehandlungsroutine zu erzeugen

Hallo Welt!
Um nun etwas zu sehen, muss man den folgenden kleinen Codeschnipsel zum Paint-Ereignishandler des glControl1-Controls hinzufügen:

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

Ein schwarzes Rechteck. Beachten Sie, dass das GLControl einen Farb- sowie einen Z-Buffer hat, den man mittels GL.Clear() leert.

Der nächste Schritt wäre das Setzen der Farbe, die genutzt wird, um den Farbbuffer zu leeren. Ein möglicher Platz für die Intialisierung ist das Load Ereignis:

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

Initialisierung des Darstellungsfelds

Um OpenGL korrekt zu nutzen, erzeugen wir zuerst eine orthografische Projektionsmatrix mit Hilfe von Gl.Ortho(). Außerdem müssen wir GL.Viewport() aufrufen.

Fürs Erste tun wir diese Aufrufe zu dem anderen Initialisierungscode in das Load-Ereignis -- dabei ignorieren wir momentan noch den Fakt, dass der Nutzer das Fenster GLControl möglicherweise in der Größe verändert will. Später wird näher auf das Vergrößern/Verkleinern eingegangen.

Die Initialisierung des Darstellungsfeldes ist hier in eine einzelne Methode ausgelager, um die Lesbarkeit zu erhöhen.

     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
    }

Und zwischen Clear() und SwapBuffers() nun unser gelbdes Dreieck:

     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!

Tastatureingaben
Nun soll das Dreieck animiert werden durch die Interaktion mit dem Nuter. Immer wenn die Leertaste gedrück wird, soll das Dreieck ein Pixel nach rechts verschoben werden.

Es gibt zwei Ansätze, um Tastatureingaben in GLControl-Szenarien abzufangen. Entweder man nutzt Windows.Forms Tastatur-Ereignisse oder man nutzt KeyboardDevice von OpenTK. Aber da der Rest des Programms Windows.Forms nutzt (unser Fenster könnte nur ein sehr kleiner Teil der großen GUI sein), werden wir in diesem Tutorial die Windows.Forms-Ereignisse nutzen.

Wir haben eine int x=0; Variable, die wir im KeyDown-Ereignis inkrementieren. Wenn man diesesn Handler zum glControl1 und nicht zur Form hinzufügt, wird man das glControl1 fokussieren müssen, z.B. indem der Nutzer in das Steuerelement hineinklickt.

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

Es wird ein GL.Translate()-Aufruf zum Paint-Ereignishandler hinzugefügt:

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

Wenn wir das Programm nun laufen lassen, geschieht nichts. Der Grund ist, dass das glControl1 nicht neu gezeichnet wird; der Fenstermanager des Betriebssystems (Windows/X/OSX) stellt sicher, dass so wenig wie möglich Paint Ereignisse auftreten. Nur beim Vergrößern/Verkleinern und ein paar mehr Situationen wird das Paint Ereignis ausgelöst.

Was wir nun tuen müssen, ist dem Fenstermanager zu sagen: "Dieses Steuerelement muss neu gezeichnet werden, da die zu Grunde liegenden Daten geändert wurden". Das geht ganz einfach mit dem Aufruf der Invalidate() Methode:

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

Fokus-Verhalten
Nun werden wir ein kleines Experiment machen, um heraus zu finden wie das Fokus-Verhalten ist.

Wir zeichnen ein gelbes Dreieck, wenn glControl1 fokussiert ist und ein blaues wenn es nicht den Fokus hat:

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

So wenn das Dreieck nun gelb ist, müsste das Drücken der Leertaste funktionieren und wenn das Dreieck blau eingefärbt ist, werden alle Tastatureingaben ignoriert.

Freiheit: Das Fenster vergrößern und verkleinern

Immer wenn sich die Größe eines Windows.Forms Steuerelement ändert, wird dessen Resize-Ereignis ausgelöst. Das gilt auch für glControl1. Das ist unser erster wichtiger Punkt.

Die zweite Frage ist, "Was müssen wir aktualisieren, wenn ein GLControl seine Größe verändert?" und die Antwort ist "Das Darstellungsrechteck und die Projektionsmatrix".

Wir können an dieser Stelle also nun wieder unsere nützliche Funktion SetupViewport nutzen. Fügen Sie also einen Ereignshandler zum Resize-Ereignis von glControl1 hinzu:

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

Es gibt jedoch immer noch ein Problem: Wenn Sie das Fenster verkleinern, wird kein Paint-Ereignis ausgelöst (wenn man z.B. das Fenster mit dem Anfasser rechts unten verkleinert). Das kommt daher, weil der Fenstermanager annimmt der (0,0) Pixel eines Steuerelements liegt in der oberen linken Ecke. Verkleinert man ein Fenster von oben links aus wird das Dreieck daher korrekt neugezeichnet. Die Lösung des Problem ist einfach: Wir instruieren den Fenstermanager ein Paint-Ereignis manuell auszulösen:

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

Wir möchen eine Hauptschleife: Eine Animation mit Hilfe von Application.Idle
Was ist, wenn wir ein Dreiecke haben wollen, welches sich kontinuierlich dreht? Normalerweise würden wir eine Variable in unserer Hauptschleife inkrementieren bevor wir da Dreieck zeichnen.

Aber wir haben keine Schleife. Wir haben lediglich Ereignisse!

Um dem auszuhelfen, müssen wir Windows.Forms dazu zwingen sich so zu verhalten wie wir es möchten. Wir benötigen ein Ereignis, welches ständig ausgelöst wird, so oft dass wir einer Echtzeit-Animation erhalten.

Es gibt mehrere Wege dies zu erreichen. Man könnte einen Timer nutzen - eine Variable im Tick Ereignis des Timer inkrementieren. Eine andere Möglichkeit ist das nutzen eines Threads. Die erste Variante ist sehr high-level und langsam während die zweite wirklich low-level und schwer zu implementieren ist.

Wir werden einen weiteren Pfad einschlagen und ein Windows.Forms Ereignis nutzen, welches genau für diesen Anwendungsfall gedacht ist: Das Application.Idle-Ereignis.

Dieses Ereignis ist in vielerlei Hinsicht sehr speziell. Es ist nicht mit einem Fenster oder einem anderem Steuerelement verknüpft, stattdessen mit dem Programm als solches. Man kann es nicht vom GUI-Designer aus nutzen, sondern muss sich manuell beim Ereignis registrieren - z.B. im Load-Ereignis:

     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)
    {
    }

Ein Vorteil des Idle-Ereignis ist, dass die dazugehörigen Ereignishandler im Windows.Forms Thread aufgerufen werden. Das bedeutet wir können alle GUI-Steuerelemente nutzen ohne uns Gedanken über Threading-Probleme zu machen .

Damit inkrementieren wir nun einfach unsere rotation Variable im Idle Ereignishandler und rufen Invalidate() für glControl1 auf.

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

Nun frischen wir unseren Code zum Zeichnen auf:

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

Es funktioniert! Siehe folgendes Bild:

Das Dreieck dreht sich langsamer wenn das Fenster groß ist! Wieso?
(Das könnte bei Ihnen eventuell nicht der Fall sein, wenn Sie einen superschnellen Computer mit einer aktuellen Grafikkarten haben; aber Sie wollen ja, dass ihr Spiel auch auf dem PC Ihres Nachbarn funktioniert, oder?)

Der Grund ist, dass 3D-Rending in Fenster meistens um einiges langsamer ist als Vollbild-Rendering.

Aber Sie können dieses Problem reduzieren, indem Sie eine Technik namens Frameraten-unabhängige Animation nutzen. Die Idee ist einfach: Inkrementieren Sie die rotation Variable nicht um 1, dafür um einen Wert der von der Zeichengeschwindigkeit abhängt (wenn die Geschwindigkeit langsam ist, um einen hohen Wert inkrementieren).

Wir müssen die Geschwindigkeit des Zeichnen messen, d.h. messen wir wie lange das Rendern eines Frames dauert.

Seit .NET 2.0 gibt es die Klasse StopWatch mit der man hoch-präzise Zeitmessungen durchführen kann. So funktioniert sie:

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

(Versuchen Sie nicht DateTime.Now zu nutzen - die Präzision liegt bei 10 oder mehr Millisekunden, ungefähr so lange wie man typischer Weise braucht, um einen Frame zu rendern ... nutzlos.)

Nun können wir die Zeit messen, die benötigt wird, um da glControl zu zeichnen. Es gibt jedoch einen eleganteren Weg: Wir messen die Zeit, die wir nicht im Application.Idle-Ereignis verbringen! Dann können wir sicher sein, dass es nicht nur das Zeichnen ist was wir messen, sondern alles, was seit dem letzten Idle-Ereignis ausgeführt wurde.

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

Cool! Das Dreieck dreht sich unabhängig von der Fenstergröße immer mit der gleichen Geschwindigkeit.

FPS-Zähler

Einene FPS-Zähler zu implementieren ist recht einfach, da wir nun eine Stoppuhr (Stopwatch) haben.

Wir zählen wie oft das Idle-Ereignis in jeder Sekunde aufgerufen wird und aktualisieren mit diesem Wert ein Label-Steuerelement. Natürlich müssen wir wissen wann eine Sekunde vorbei ist, daher brauchen wir eine Variable, um alle "Zeitstücken" zu addieren.

Ziemlich viel Logik befand sich im Idle Ereignis, daher wurde der Code nun etwas aufgesplittet:

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

Unser FPS-Zähler in all seiner Pracht:

Kompletter Quellcode

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

Faustregeln

This page is the German Translation of Rules of thumb.

Dieser Text ist gedacht für Leute, die bisher mit C/OpenGL gearbeitet haben.

Obwohl OpenTK automatisch alle GL/AL Funktionsaufrufe von C# nach C umwandelt, arbeiten einige Dinge in der Welt der verwalteten Sprachen etwas anders vergleichen mit simplem C.

Faustregeln

  1. Bevorzugen Sie Server-Speicher statt Klientspeicher

    Ein paar alte OpenGL-Funktionen nutzen Zeiger auf Speicher, der vom Nutzer verwaltet ist. Das bekannteste Beispiel sind Vertex Arrays - die Familie der GL.*Pointer-Funktionen.

    Dieser Ansatz kann in einer Garbage Collector Umgebung (wie .NET es ist) nicht genutzt werden, da der Garbage Collector (GC) die Inhalte des Buffers eventuell verschiebt. Es ist sehr empfehlenswert alte Vertex Arrays mit Vertex Buffer Objects zu ersetzen, welche dieses Problem nicht haben.

    Anders als OpenGL 2.1, wird OpenGL 3.0 keinerlei solcher Funktionen zur Nutzung des Klientspeichers beinhalten.

  2. Versuchen Sie die Anzahl der OpenGL-Funktionsaufrufe per Frame zu minimieren

    Dies ist für jede Programmierumgebung wichtig, die OpenGL nutzt, für OpenTK jedoch sogar noch ein wenig wichtiger: Obwohl die OpenGL/AL Bindings ziemlich optimiert sind, kostet der Übergang von der verwalteten zur unverwalteten Welt einen kleinen, aber messbaren, Overhead (Einbußen an Performance).

    Um den Einfluss des Overheads zu minimieren, versuchem Sie die Anzahl der OpenGL/OpenAL Aufrufe zu verringern. Eine gute Faustregel ist es nicht mehr als 300-500 OpenGL-Aufrufe per Frame zu haben. Dies kann durch Vermeidung des Immediate Mode zu Gunsten von Display Listen oder VBO's geschehen.

  3. Nutzen Sie ref Funktions-Überladungen für maximale Performance bei Mathe-Routinen
    Dies ist wichtig weil Vector3, Matrix3 etc. Strukturen und keine Klassen sind. Klassen-Objekte werden standardmäßig per Referenz übergeben.

    Vector3 v1 = Vector3.UnitX;
    Vector3 v2 = Vector3.UnitZ;
    Vector3 v3 = Vector3.Zero;
    v3 = v1 + v2;                        // requires three copies; slow.
    Vector3.Add(ref v1, ref v2, out v3); // nothing is copied; fast!

    Das gleiche trifft für das Aufrufen von OpenGL-Funktionen zu:

    GL.Vertex3(ref v1.X);  // pass a pointer to v1; fast!
    GL.Vertex3(v1);        // copy the whole v1 structure; slower!

Performance messen

  1. GameWindows besitzt einen eingebauten Frames-pro-Sekunde-Zähler. Wie auch immer, GLControl hat keinen.
  2. Eine einfache und bequeme Möglichkeit zur Messung der Performance Ihres Codes stellt die .NET 2.0 / Mono 1.2.4 Stopwatch Klasse dar. Benutzen Sie sie wie folgt:
    Stopwatch sw = new Stopwatch();
    sw.Start();
    // Your code goes here.
    sw.Stop();
    double ms = sw.Elapsed.TotalMilliseconds;

    Anmerkung: Vermeiden Sie die Nutzung von DateTime.Now oder anderer DateTime-basierte Methoden für Zeitspannen unter ein paar Sekunden, da die Präzisision bei 10ms oder schlecher liegt. (Gerüchte besagen, dass es von Zeit zu Zeit sogar schlechter wird!) DateTime für das Messen von langen Operationen (einige Sekunden) zu nutzen ist OK.

  3. Wenn Sie Windows nutzen, können Sie Fraps downloaden, um die Anzahl der Frames pro Sekunde zu messen, die in Ihrer Anwendung gezeichnet werden.

Referenzen:
http://www.gamedev.net/community/forums/topic.asp?topic_id=484756&whichp...

OpenGL

Um OpenGL-Funktionen zu nutzen, muss Ihr System entsprechende Treiber zur Hardware-Beschleunigung besitzen.

Das OpenGL Red Book ist ein von Silicon Graphics Ingenieuren verfasstes Buch und wird Sie in die Grafikprogrammierung einführen. Es ist sehr empfehlenswert sich dieses Buch anzusehen und die essentiellen Konzepte von OpenGL zu erlernen.

Diese Seiten sind eher auf OpenTK spezifische Änderungen zur C-API ausgerichtet und darauf wie man die OpenTK.Utility Klassen nutzt.