Chapter 2: OpenTK Classes

Todo:

2. OpenTK Classes
2.1 OpenTK.Input (should make clear how to query and interpret the input devices)
2.2 OpenTK.Math (should make clear for what tasks the types can be used, rather than a full vector math explanation)
2.3 Rendering Context (creation, differences etc)
2.4 OpenTK.Fonts & Timing (Show how to print static text, show how to make a Frame/Sec counter)
2.5 OpenTK.OpenGL (setting up a window, Geometry, Shaders, States, Textures, Viewing etc.)
2.6 OpenTK.OpenAL [75%]

Building a Windows.Forms+GLControl based application

This tutorial assumes familiarity with Windows.Forms application development in Visual Studio 2005/C#, and at least basic knowledge of OpenGL. It also assumes a top-to-bottom readthrough; it is a guide and not a reference.

To begin with, it is quite a different approach one has to take when designing a game/application using the GLControl in a Windows.Form compared to using the GameWindow. GLControl is more low-level than GameWindow so you'll have to pull the strings on for example time measurements by yourself. In GameWindow, you get more for free!

Just as in the GameWindow case, GLControl uses the default OpenGL driver of the system, so with the right drivers installed it will be hardware accelerated. However, with large windows it will be slower than the corresponding fullscreen GameWindow, because of how the underlying windowing system works [someone with more detailed knowledge than me may want to elaborate on this..].

If you come from a "main-loop-background" (C/SDL/Allegro etc.) when it comes to coding games, you'll have to rethink that fundamentally. You'll have to change into a mindset of "what event should I hook into, and what events should I trigger, and when?" instead.

Why use Windows.Forms+GLControl instead of GameWindow?
The first thing you'll have to decide is:

"Do I really need the added complexity of Windows.Forms + embedded GLControl compared to a windowed GameWindow?"

Here are some reasons why you would like to add that complexity:

  1. You want to build a GUI-rich/RAD kind of application using the Windows.Forms controls. Eg. level editors or model viewers/editors may be in this category while a windowed game leans more towards a GameWindow kind of application.
  2. You want to have an embedded OpenGL rendering panel inside an already existing Windows.Forms application.
  3. You want to be able to do drag-and-drop into the rendering panel, for example dropping a model file into a model viewer.

Assuming you've got at least one of those reasons to build a Windows.Forms+GLControl based application, here's the steps, gotchas and whys for you.

Adding the GLControl to your Windows.Form
(I am assuming you are using Visual Studio 2005 Express Edition. Your mileage may vary if using VS2008 or Monodevelop -- I don't know the details for them -- but the follow sections should be the same no matter how you add the GLControl)
To begin with, create a Form on which you will place your GLControl. Right click in some empty space of the Toolbox, pick "Choose Items..." and browse for OpenTK.dll. Make sure you can find the "GLControl" listed in the ".NET Framework Components", as in the image below.

Then you can add the GLControl to your form as any .NET control. A GLControl named glControl1 will be added to your Form.

The first thing you'll notice is the "junk graphics" inside glControl1. Under the hood, GLControl uses a so called GLContext to do the actual GL-rendering and so on, and this context is only created in runtime, not in design time. So no worries.

Order of creation
The fact that glControl1's GLContext is created in runtime is important to remember however, since you cannot access or change glControl1's properties reliably until the GLContext has been created. The same is true for any GL.* commands (or Glu for that matter!). The conceptual order is this:

  1. First the Windows.Form constructor runs. Don't touch glControl/GL
  2. Then the Load event of the form is triggered. OK to touch glControl/GL
  3. After the Load event handler has run, any event handler may touch glControl/GL.

So one approach to address this problem is having a bool loaded=false; member variable in your Form, which is set to true in the 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;
    }
  }

Then in any event handler where you are going to access glControl1/GL you can put a guard first of all:

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

A popular way of adding a Load event handler to a Form is via the Properties window of Visual Studio, something like this:

  1. Click the Form1's title bar in the Designer
  2. Make sure Form1 is listed in the Properties window
  3. Click the "Events" button to list all events of Form1
  4. Double-click the empty cell right of the Load event to create and hook an event handler for the Load event

Hello World!
The absolutely minimal code you can add at this stage to see something is adding an event handler to the Paint event of glControl1 and filling it with this:

    private void glControl1_Paint(object sender, PaintEventArgs e)
    {
      if (!loaded) // Play nice
        return;

      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
      glControl1.SwapBuffers();
    }

Yes! A black viewport. Notice that the GLControl provides a color- and a depth buffer, which we have to clear using GL.Clear(). [TODO: how to control which buffers and formats the GLControl has? Possible at all?]

Next thing would be setting the clear color. An appropriate place to do GL initialization is in the form's 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 initialization
Next thing we want to do is draw a single yellow triangle.

First we need to be good OpenGL citizen and setup an orthographic projection matrix using GL.Ortho(). We need to call GL.Viewport() also.

For now we'll add this in the Load event handler by the other initialization code -- ignoring the fact that we may want to allow the user to resize the window/GLControl. We'll look into window resizing later.

I put the viewport initialization in a separate method to make it a little more readable.

    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
    }

And between Clear() and SwapBuffers() our yellow triangle:

    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!

Keyboard input
Next thing we want to do is animate the triangle via user interaction. Every time Space is pressed, we want the triangle to move one pixel right.

The two general approaches to keyboard input in a GLControl scenario is using Windows.Forms key events and using the OpenTK KeyboardDevice. Since the rest of our program resides in the Windows.Forms world (our window might be a very small part of a large GUI) we'll play nice and use Windows.Forms key events in this guide.

We'll have an int x=0; variable that we'll increment in a KeyDown event handler. Adding it to the glControl1 and not the Form, means the glControl has to be focused, ie. clicked by the user for key events to be sent to our handler.

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

We add GL.Translate() to our 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

      ...
    }

.. and we run our program. But wait! Nothing happens when we push Space! The reason is, glControl1 is not painted all the time; the operating systems window manager (Windows/X/OSX) makes sure as few Paint events as possible happens. Only on resize, obscured window and a couple of more situations do Paint events actually get triggered.

What we would like to do is have a way of telling the window manager "This control needs to be repainted since the data it relies on has changed". We want to notify the window manager that our glControl1 should be repainted. Easy, Invalidate() to the resque:

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

Focus behaviour
If you're anything like me you're wondering a little how this focusing behaves; let's find out!

A simple way is painting the triangle yellow when glControl1 is focused and blue when it is not. Right:

    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 now anytime the triangle is yellow, Space should work alright, and when it's blue any keyboard input will be ignored.

Freedom at last: resizing the window
We will now turn our attention to a sore teeth: window resizing.

Anytime a Windows.Forms control is resized, it's Resize event is triggered. That is true for glControl1 too. That's one piece in the puzzle.

The other piece to find is "What do we need to update when a GLControl is resized?" and the answer is "The viewport and the projection matrix".

Well it seems our SetupViewport() will come in handy once more! Add an event handler to the Resize event of glControl1 and fill it in:

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

There is still one problem though: if you shrink the window using eg. the bottom-right resize grip of the window, no repaint will trigger automatically. That's because the window manager makes assumptions about where the (0, 0) pixel of a control resides, namely in the top-left corner of the control. (Try resizing using the top-left grip instead - the triangle is repainted continously!). Our general fix to alleviate this problem is instructing the window manager that we really want the repaint to occur upon any resize event:

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

I want my main loop: driving animation using Application.Idle
So, what if we wanted our triangle to rotate continously? This would be childs play in a main loop scenario: just increment a rotation variable in the main loop, before we render the triangle.

But we don't have any loop. We only have events!

To mend for this lack of continuity we have to force Windows.Forms to do it our way. We want an event triggered every now and then, fast enough to get that realtime interactive feeling.

Now there are several ways to achieve this. One is adding a Timer control to our form, changing rotation in the timers Tick event handler. Another is adding a wild Thread to the soup. The first is a little too high-level and slow while the second is really low-level and a bit harder to get right.

We will take a third path and use a Windows.Forms event designed just for the purpose of being executed "when nothing else is going on": meet the Application.Idle event.

This event is special in a number of ways as you may have guessed already. It is not associated with any Form or other control, but with the program as a whole. You cannot hook into it from the GUI Designer; you'll have to add it manually -- for example in the 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)
    {
    }

One good thing about the Idle event is that the corresponding event handlers are executed on the Windows.Forms thread. That is good since it means we can access all GUI controls without having to worry about threading issues, a pain we would have to deal with if we cooked our own thread.

So we simply increment our rotation variable in the Idle event handler and Invalidate() glControl1 -- business as usual.

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

Let's update our rendering code while we're at it:

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

Happy wonders! Look:

The triangle rotates slower when the window is big! How come?
(This might not be true if you have a super-fast-computer with a super-fast-graphics-card; but you want your game to run on your neighbours computer too, don't you?)

The reason is that windowed 3d rendering just is a lot slower than full-screen rendering, in general.

But you can reduce the damage by using a technique called frame-rate independent animation. The idea is simple: increment the rotation variable not with 1 but with an amount that depends on the current rendering speed (if the speed is slow, increment rotation with a larger amount than if the speed is high).

But you need to be able to measure the current rendering speed or, equivalently, how long time it takes to render a frame.

Since .NET2.0 there is a class available to do high-precision time measurements called Stopwatch. Here's how to use it:

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

(don't try with DateTime.Now -- it has a granularity of 10 or more milliseconds, which is in the same size as typical frame rendering -- worthless..)

Now, if we could measure the time it takes to perform the glControl painting, we would be close to making some kind of frame-rate-independent animation. But there is an even more elegant way: let's measure all time that is not Application.Idle time! Then we'll be sure it is not just the painting that is measured, but everything that has been going on since our last Idle run:

    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! The triangle spins with the same speed regardless of window size.

I want an FPS counter!
Yeah me too. It's quite simple now that we've got a Stopwatch.

The idea is just counting the Idle runs, and every second or so, update a Label control with the counter! But we'll have to know when a second has passed, so we need an accumulator variable adding all time slices together.

Quite a lot of logic started to add up in the Idle event handler, so I split it up a little:

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

Our FPS counter in all its glory:

Can't you put the complete source code somewhere?
Sure, here it is:

using System;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using OpenTK.OpenGL;
using OpenTK.OpenGL.Enums;
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();
    }
  }
}

OpenAL

The OpenAL 1.1 Crashcourse will give an introduction how to use OpenAL in your applications.

OpenAL contains the following classes:

  • AL "Audio Library"
  • Alc "Audio Library Context"
  • Alut "Audio Library Utilities"
  • XRam "Memory Extension"
  • Efx "Effects Extension"

OpenAL 1.0 Extensions that were included into 1.1: Multi-Channel Buffer playback Extension, Audio Capture Extension, Enumeration Extension.

It is recommended using these book pages as a starting point, and visit the online resources from the OpenAL website's documentation page for in-depth information. Downloading the OpenAL SDK is not required, but will provide you with some .wav files to toy around with and a few .pdf files not available directly at the OpenAL site.

Regarding compatibility, the "Generic Software" and "Generic Hardware" implementations of the OpenAL driver support OpenAL 1.1 and a few EFX Extensions, namely the Reverb Effect and Lowpass Filter. If the used Device cannot handle EAX natively, the driver will attempt to emulate the missing features.

Note that some functions of the OpenAL API are not imported for safety reasons. Rather use .Net's Thread.Sleep than Alut.Sleep, and Alut.CreateBuffer* instead of Alut.LoadMemory*. If this is a Problem, please voice it in the forum.

1. Devices, Buffers and X-Ram

Alut hides Device and Context allocation through Alc, and adds means to load or generate sound data. We will use Alut for all examples, until OpenTK includes a wrapper.

Alut.Init() will initialize the default Device and Context and makes it current. Calling Alut.Exit() at the end of the program will destroy the Device and Context.

Buffers
Buffers in OpenAL are merely the storage for audio data in raw format, a Buffer Name (often called Handle) must be generated using AL.GenBuffers(). This buffer can now be filled using AL.BufferData(), or using the Alut.CreateBufferFromFile (which loads a file from disk) or Alut.CreateBufferWaveform (which generates noise through parameters). The Alut functions imply the step of using AL.BufferData() to pass the raw sound data into OpenAL's memory.

X-Ram
The X-Ram Extension allows to manually assign Buffers a storage space, it's use is optional and not required. To use the Extension, the XRam wrapper must be instantiated (per used Device), which will take care of most ugly things with Extensions for you. The instantiated object contains a bool that returns if the Extension is usable, which should be checked before calling one of the Extension's Methods.

Example code:

bool success = Alut.Init();
if ( !success || AL.GetError() != ALError.NoError)
{ // problem with Device or Context, cannot continue
   Alut.Exit();
   return;
}

XRam = new XRamExtension( ); // must be instantiated per used Device if X-Ram is desired.

// reserve 2 Handles
uint[] MyBuffers = new uint[2];
AL.GenBuffers( 2, out MyBuffers );

// Load a .wav file from disk
if ( XRam.IsInitialized ) XRam.SetBufferMode( ref MyBuffer[0], XRamStorage.Hardware ); // optional
MyBuffers[0] = Alut.CreateBufferFromFile( "something.wav" );
if ( AL.GetError() != ALError.NoError )
{
   // respond to load error etc.
}

// Create a sinus waveform through parameters
if ( XRam.IsInitialized ) XRam.SetBufferMode( ref MyBuffer[1], XRamStorage.Hardware ); // optional
MyBuffers[1] = Alut.CreateBufferWaveform(AlutWaveform.Sine, 500f, 42f, 1.5f);

// See next book page how to connect the buffers to sources in order to play them.

// Cleanup on application shutdown
AL.DeleteBuffers( 2, ref MyBuffers ); // free previously reserved Handles
Alut.Exit();

A description of the sound data in the Buffer can be queried using AL.GetBuffer().

Now that the Buffer Handle has been assigned a sound, we need to attach it to a Source for playback.

2. Sources and EFX

Sources
Sources represent the parameters how a Buffer Object is played back. These parameters include the Source's Position, Velocity, Gain (Volume amplification) and more. The settings can be set/get by using AL.Source and AL.GetSource functions.

Continuation of the sourcecode from previous page:

uint[] MySources = new uint[2];
AL.GenSources( 2, out MySources ); // gen 2 Source Handles

AL.Source( TestSources[0], ALSourcei.Buffer, (int)MyBuffers[0] ); // attach the buffer to a source

AL.SourcePlay( MySources[0]); // start playback
AL.Source( MySources[0], ALSourceb.Looping, true ); // source loops infinitely

AL.Source( MySources[1], ALSourcei.Buffer, (int)MyBuffers[1] );
Vector3 Position = new Vector3( 1f, 2f, 3f );
AL.Source( MySources[1], ALSource3f.Position, ref Position );
AL.Source( MySources[1], ALSourcef.Gain, 0.85f );
AL.SourcePlay( MySources[1] );

Console.ReadLine(); // wait for keystroke before exiting

AL.SourceStop( MySources[0] ); // halt playback
AL.SourceStop( MySources[1] );

AL.DeleteSources( 2, ref MySources ); // free Handles
// now delete Buffer Objects and Alut.Exit();

EFX

I'm sorry to do this, but if you want to work with EFX there is no other way. All I can give here is a brief overview that might help you make the decision if EFX is what you need. You will have to download the OpenAL SDK to get a copy of "Effects Extension Guide.pdf" from Creative labs, for in-depth information about programming with DSPs.

My advice is ignoring EFX, unless your game project is in 1st Person 3D. Environmental effects might look nice as a "selling point" on paper, but do not add any gameplay value to a Strategy game, or a 2D platform game.

The addition to OpenAL with EFX Extension is the rerouting of output signals.
-In vanilla OpenAL you load a Buffer, attach it to a Source and besides the Source's parameters that's all the influence you have about what ends up in the mixer.
-With EFX you may reroute a source's output through Filters and/or into Auxiliary Effect Slots. This allows more fine control about how a Source sounds when played, which is useful to achieve the effect of obstruction, occlusion or exclusion of a sound due to environment features like walls, obstacles or doors.

The new OpenAL Objects that come with EFX are "Effect", "Auxiliary Effect Slot" and "Filter".

An Effect Object stores the type of effect and the values for parameters of that effect. Types of Effects are for example Echo, Distortion, Chorus, Flanger etc.

Auxiliary Effect Slots are containers for Effect Objects, whose output goes directly into the final output mix. The Slots are only used if there is a valid Effect Object attached to them, binding the reserved Handle 0 to a Slot will detach the previously bound Effect Object from it.

A Filter can be attached to a source, and either filter the "dry signal" that goes directly into the output mixer, or filter the "wet signal" that is forwarded to an Auxiliary Effect Slot.

3. Context and Listener

Like in OpenGL, a Context can be understood as an instance of OpenAL State. You can create multiple Contexts per Device, but each Context has the restriction of 1 Listener it's own unique Sources. Buffer Objects on the other hand may be shared by Contexts, which use the same Device.

Note that in contrast to OpenGL, OpenAL does not have an equivalent to SwapBuffers(). A Sources are automatically played until the end of their attached Buffer is reached, or until the programmer manually stops the Source playback.

Listener
The Listener represents the position and orientation of the Camera in the environment, thus there can be only one per Context. The settings can be set/get by using AL.Source and AL.GetSource functions.

It makes sense to handle it together with your OpenGL camera, to make sure a Source is properly positioned. This is very similar to OpenGL's Projection Matrix, with the exception that there is no Frustum culling for audio (you may not see something behind you, but you can hear it).

A sample Camera, taken from the OpenAL manual:

void PlaceCamera(Vector3 ListenerPosition, float listenerAngle)
{
  // prepare some calculations
  float Sinus = (float)Math.Sin(listenerAngle);
  float Cosinus = (float)Math.Cos(listenerAngle);
  Vector3 ListenerTarget = new Vector3(ListenerPosition.X + Sinus, ListenerPosition.Y, ListenerPosition.Z - Cosinus);
  Vector3 ListenerDirection = new Vector3(Sinus, 0, Cosinus);

  // update OpenGL - camera position
  GL.MatrixMode(MatrixMode.Projection);
  GL.LoadIdentity();
  GL.Frustum(-0.1333, 0.1333, -0.1, 0.1, 0.2, 50.0);
  Glu.LookAt(ListenerPosition, ListenerTarget, Vector3.UnitY);

  // update OpenAL - place listener at camera
  AL.Listener(ALListener3f.Position, ref ListenerPosition);
  AL.Listener(ALListenerfv.Orientation, ref ListenerDirection, ref Vector3.UnitY);
}

OpenGL

In order to use OpenGL functions, your System requires appropriate drivers for hardware acceleration.

The OpenGL Programming Guide is a book written by Silicon Graphics engineers and will introduce the reader into graphics programming. It is highly recommended you take a look at this resource to learn about the essential concepts in OpenGL.

These pages are more focused about OpenTK specific changes to the C API, and how to use OpenTK.Utility classes to assist with some common tasks.

Todo: Missing Pages

  • window-related, etc. (brings the reader to the level of a Quickstart Template)
  • GLSL related changes.
  • OpenTK.Utility related

Geometry

Here we'll discuss how to define, reference or draw geometric Objects using OpenTK.OpenGL.

Focus is on storing the Geometry directly in Video Memory using Vertex Buffer Objects (VBO). This is exactly what we want to store static parts of the Environment.

[expand with]
* 6. Drawing Optimizations

1. The Vertex

A Vertex (pl. Vertices) specifies a number of Attributes associated with a single Point in space. In the fixed-function environment a Vertex commonly includes Position, Normal, Color and/or Texture Coordinates. The only Attribute that is not optional an must be specified is the Vertex's Position, usually consisting of 3 float.
In Shader Program driven rendering it is also possible to specify custom Vertex Attributes which are previously unknown to OpenGL, such as Radius or Bone Index and Weight for Skeletal Animation. For the sake of simplicity we'll re-create one of the Vertex formats OpenGL already knows, namely InterleavedArrayFormat.T2fN3fV3f. This format contains 2 float for Texture Coordinates, 3 float for the Normal direction and 3 float to specify the Position.

Thanks to the included Math-Library in OpenTK, we're allowed to specify an arbitrary Vertex struct for our requirements, which is much more elegant to handle than a float[] array.

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

This leads to a Vertex consisting of 8 float, or 32 byte. We can now declare an Array of Vertices to describe multiple Points and allow easy indexing/referencing them.

Vertex[] Vertices;

The Vertex-Array Vertices can now be created and filled with data. Addressing elements is as convenient as in the following example:

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;

An Index is simply a byte, ushort or uint, referencing an element in the Vertices Array. So if we decide to draw a single Vertex 100 times at the same spot, instead of storing 100 times the same Vertex in Vertices, we can reference it 100 times from the Indices Array:

uint[] Indices;

Basically the Indices Array is used to describe the primitives and the Vertex Array is used to declare the corner points.

We can also use collections to store our Vertices, but it's recommended you stick with a simple Array to make sure your Indices are valid at all times.
Now the Vertices and Indices Arrays can be used to describe the edges of any Geometric Pritimitve Type.

Once the Arrays are filled with data it can be drawn in Immediate Mode, as Vertex Array or sent into a Vertex Buffer Object.

2. Geometric Primitive Types

OpenGL requires you to specify the Geometric Primitive Type of the Vertices you wish to draw. This is usually expected when you begin drawing in either Immediate Mode (GL.Begin), GL.DrawArrays or GL.DrawElements.

Geometric Primitive Types in OpenTK.OpenGL

Fig. 1: In the above graphic all valid Geometric Primitive Types are shown, their winding is Clockwise (irrelevant for Points and Lines).

This is important, because drawing a set of Vertices as Triangles, which are internally set up to be used with Quads, will result only in garbage being displayed.
Examine Figure 1, you will see that v3 in a Quad is used to finish the shape, while Triangles uses v3 to start the next shape. The next drawn Triangle will be v3, v4, v5 which isn't something that belongs to any surface, if the Vertices were originally intended to be drawn as Quads.

However Points and Lines are an Exception here. You can draw every other Geometric Primitive Type as Points, in order to visualize the Vertices of the Object. Some more possibilities are:

  • QuadStrip, TriangleStrip and LineStrip can be interchanged, if the source data isn't a LineStrip.
  • Quads can be drawn as Lines with the restriction that there are no lines between v1, v2 and v3, v0.
  • Polygon can be drawn as LineLoop
  • TriangleFan can be drawn as Polygon or LineLoop

The smallest common denominator for all filled surfaces (i.e. no Points or Lines) is the Triangle. This Geometric Primitive Type has the special attribute of always being planar and is currently the best way to describe a 3D Object to GPU hardware.
While OpenGL allows to draw Quads or Polygons aswell, it is quite easy to run into lighting problems if the surface is not perfectly planar. Internally, OpenGL breaks Quads and Polygons into Triangles, in order to rasterize them.

  1. Points
    Specifies 1 Point per Vertex v, thus this is usually only used with GL.DrawArrays().
    n Points = Vertex * (1n);
  2. Lines
    Two Vertices form a Line.
    n Lines = Vertex * (2n);
  3. LineStrip
    The first Vertex issued begins the LineStrip, every consecutive issued Vertex marks a joint in the Line.
    n Line Segments in the Strip = Vertex * (1+1n)
  4. LineLoop
    Same as LineStrip, but the very first and last issued Vertex are automatically connected by an extra Line segment.
    n Line Segments in the Loop = Vertex * (1n);
  5. Polygon
    Note that the first and the last Vertex will be connected automatically, just like LineLoop.
    Polygon with n Edges = Vertex * (1n);
    Note: This primitive type should really be avoided whenever possible, basically the Polygon will be split to Triangles in the end anyways. Like Quads, polygons must be planar or be displayed incorrectly. Another Problem is that there is only 1 single Polygon in a begin-end block, which leads to multiple draw calls when drawing a mesh, or using the Extensions GL.MultiDrawElements or GL.MultiDrawArrays.
  6. Quads
    Quads are especially useful to work in 2D with bitmap Images, since those are typically rectangular aswell. Care has to be taken that the surface is planar, otherwise the split into Triangles will become visible.
    n Quads = Vertex * (4n);
  7. QuadStrip
    Like the Triangle-strip, the QuadStrip is a more compact representation of a sequence of connected Quads.
    n Quads in Quadstrip = Vertex * (2+2n);
  8. Triangles
    This way to represent a mesh offers the most control over how the Triangles are sorted, a Triangle always consists of 3 Vertex.
    n Triangles = Vertex * (3n);
    Note: It might look like an inefficient brute force approach at first, but it has it's advantages over TriangleStrip. Most of all, since you are not required to supply Triangles in sequenced strips, it is possible to arrange Triangles in a way that makes good use of the Vertex Caches. If the Triangle you currently want to draw shares an edge with one of the Triangles that have been recently drawn, you get 2 Vertices, that are stored in the Vertex Cache, almost for free. This is basically the same what stripification does, but you are not restricted to a certain Direction and forced to insert degenerated Triangles.
  9. TriangleStrip
    The idea behind this way of drawing is that if you want to represent a solid and closed Object, most neighbour Triangles will share 2 Vertices (an edge). You start by defining the initial Triangle (3 Vertices) and after that every new Triangle will only require a single new Vertex for a new Triangle.
    n Triangles in Strip = Vertex * (2+1n);
    Note: While this primitive type is very useful for storing huge meshes (2+1n Vertices per strip as opposed to 3n for BeginMode.Triangles), the big disadvantage of TriangleStrip is that there is no command to tell OpenGL that you wish to start a new strip while inside the glBegin/glEnd block. Ofcourse you can glEnd(); and start a new strip, but that costs API calls. A workaround to avoid exiting the begin/end block is to create 2 or more degenerate Triangles (you can imagine them as Lines) at the end of a strip and then start the next one, but this also comes at the cost of processing Triangles that will inevitably be culled and aren't visible. Especially when optimizing an Object to be in a Vertex Cache friendly layout, it is essential to start new strips in order to reuse Vertices from previous draws.
  10. TriangleFan
    A fan is defined by a center Vertex, which will be reused for all Triangles in the Fan, followed by border Vertices. It is very useful to represent convex n-gons consisting of more than 4 vertices and disc shapes, like the caps of a cylinder.

When looking at the graphic, Triangle- and Quad-strips might look quite appealing due to their low memory usage. They are beneficial for certain tasks, but Triangles are the best primitive type to represent an arbitrary mesh, because it's not restricting locality and allows further optimizations. It's just not realistic that you can have all your 3D Objects in Quads and OpenGL will split them internally into Triangles anyway. 3 ushort per Triangle isn't much memory, and still allows to index 64k unique Vertex in a mesh, the number of Triangles can be much higher. Don't hardwire BeginMode.Triangles into your programs though, for example Quads are very commonly used in orthographic drawing of UI Elements such as Buttons, Text or Sprites.

Should TriangleStrip get an core/ARB command to start a new strip within the begin/end block (only nVidia driver has such an Extension to restart the primitive) this might change, but currently the smaller data structure of the strip does not make up for the performance gains a Triangle List gets from Vertex Cache optimization. Ofcourse you can experiment with the GL.MultiDraw Extension mentioned above, but using it will break using other Extensions such as DirectX 10 instancing.

3.a Vertex Buffer Objects

Introduction
The advantage of VBO (Vertex Buffer Objects) is that we can tell OpenGL to store information used for drawing - like Position, Colors, Texture Coordinates and Normals - directly in the Video-card's Memory, rather than storing it in System Memory and pass it to the graphics Hardware every time we wish to draw it. While this has been already doable with Display Lists before, VBO has the advantage that we're able to retrieve a Pointer to the data in Video Memory and read/write directly to it, if necessary. This can be a huge performance boost for dynamic meshes and is for years the best overall solution for storing - both, static and dynamic - Meshes.

Creation
Handling VBOs is very similar to handling Texture objects, we can generate/delete handles, bind them or fill them with data. For this tutorial we will need 2 objects, one VBO containing all Vertex information (Texture, Normal and Position in this example case) and an IBO (Index Buffer Object) referencing Vertices from the VBO to form Triangles. This has the advantage that, when we have uploaded the data to the VBO/IBO later on, we can draw the whole mesh with a single GL.DrawElements call.

First we acquire two Objects to use:

uint[] VBOid = new uint[ 2 ];
GL.GenBuffers( 2, out VBOid );

Although it is unlikely, OpenGL could complain that it ran out of memory or that the extension is not supported, it should be checked with GL.GetError. If everything went smooth we have 2 objects to work with available now.

Delete
The OpenGL driver should clean up all our mess when it deletes the render context, it's always a good idea to clean up on your own where you can. We remove the objects we reserved at the buffer creation by calling:

GL.DeleteBuffers( 2, ref VBOid );

Binding
To select which object you currently want to work with, simply bind the handle to either BufferTarget.ArrayBuffer or BufferTarget.ElementArrayBuffer. The first is used to store position, uv, normals, etc. (named VBO) and the later is pointing at those vertices to define geometry (named IBO).

GL.BindBuffer( BufferTarget.ArrayBuffer, VBOid[ 0 ] );
GL.BindBuffer( BufferTarget.ElementArrayBuffer, VBOid[ 1 ] );

It is not required to bind a buffer to both targets, for example you could store only the vertices in the VBO and keep the indices in system memory. Also, the two objects are not tied together in any way, for example you could build different triangle lists for BufferTarget.ElementArrayBuffer to implement LOD on the same set of vertices, simply by binding the desired element array.

Theres two important things to keep in mind though:

1) While working with VBOs, GL.EnableClientState(EnableCap.VertexArray); must be enabled. if using Normals, GL.EnableClientState(EnableCap.NormalArray), just like classic Vertex Arrays.

2) All Vertex Array related commands will be used on the currently bound objects until you explicitly bind zero '0' to disable hardware VBO.

GL.BindBuffer( BufferTarget.ArrayBuffer, 0 );
GL.BindBuffer( BufferTarget.ElementArrayBuffer, 0 );

Passing Data
There are several ways to fill the object's data, we will focus on using GL.BufferData and directly writing to video memory. The third option would be GL.BufferSubData which is quite straightforward to use once you are familiar with GL.BufferData.

  1. GL.BufferData
    We will start by preparing the IBO, it would not make a difference if we set up the VBO first, we simply start with the shorter one.

    We make sure the correct object is bound (it is not required to do this, if the buffer is already bound. Just here to clarify on which object we currently work on)

    GL.BindBuffer( BufferTarget.ElementArrayBuffer, VBOid[ 1 ] );

    In the example application ushort has been used for Indices, because 16 Bits [0..65535] are more available Vertices than used by most real-time rendered meshes, however the mesh could index way more Vertices using a type like uint. Using ushort, OpenGL will store this data as 2 Bytes per index, saving memory compared to a 4 Bytes UInt32 per index.

    The function GL.BufferData's first parameter is the target we want to use, the second is the amount of memory (in bytes) we need allocated to hold all our data. The third parameter is pointing at the data we wish to send to the graphics card, this can be IntPtr.Zero and you may send the data at a later stage with GL.MapBuffer (more about this later). The last parameter is an optimization hint for the driver, it will place your data in the best suited place for your purposes.

    GL.BufferData( BufferTarget.ElementArrayBuffer, (IntPtr) ( Indices.Length * sizeof( ushort ) ), Indices, BufferUsageHint.StaticDraw );

    That's all, OpenGL now has a copy of Indices available and we could dispose the array, assuming we have the Index Count of the array stored in a variable for the draw call later on.

    Now that we've stored the indices in an IBO, the Vertices are next. Again, we make sure the binding is correct, give a pointer to the Vertex count, and finally the usage hint.

    GL.BindBuffer( BufferTarget.ArrayBuffer, VBOid[ 0 ] );
    GL.BufferData( BufferTarget.ArrayBuffer, (IntPtr) ( Vertices.Length * 8 * sizeof( float ) ), Vertices, BufferUsageHint.StaticDraw );

    There's a table at the bottom of this page, explaining the options in the enum BufferUsageHint in more detail.

  2. GL.MapBuffer / GL.UnmapBuffer

    While the first described technique to pass data into the objects required a copy of the data in system memory, this alternative will give us a pointer to the video memory reserved by the object. This is useful for dynamic models that have no copy in client memory that could be used by GL.BufferData, since you wish to rebuild it every single frame (e.g. fully procedural objects, particle system).

    First we make sure that we got the desired object bound and reserve memory, the pointer towards the Indices is actually IntPtr.Zero, because we only need an empty buffer.

    GL.BindBuffer( BufferTarget.ElementArrayBuffer, VBOid[ 0 ] );
    GL.BufferData( BufferTarget.ElementArrayBuffer, (IntPtr) ( Indices.Length * sizeof( ushort ) ), IntPtr.Zero, BufferUsageHint.StaticDraw );

    Note that you should change BufferUsageHint.StaticDraw properly according to what you intend to do with the Data, there's a table at the bottom of this page. Now we're able to request a pointer to the video memory.

    IntPtr VideoMemoryIntPtr = GL.MapBuffer(BufferTarget.ElementArrayBuffer, BufferAccess.WriteOnly);

    Valid access flags for the pointer are BufferAccess.ReadOnly, BufferAccess.WriteOnly or BufferAccess.ReadWrite, which help the driver understand what you're going to do with the data. Note that the data's object is locked until we unmap it, so we want to keep the timespan over which we use the pointer as short as possible. We may now write some data into the buffer, once we're done we must release the lock.

    unsafe
    {
      fixed ( ushort* SystemMemory = &Indices[0] )
      {
        ushort* VideoMemory = (ushort*) VideoMemoryIntPtr.ToPointer();
        for ( int i = 0; i < Indices.Length; i++ )
          VideoMemory[ i ] = SystemMemory[ i ]; // simulate what GL.BufferData would do
      }
    }
    GL.UnmapBuffer( BufferTarget.ElementArrayBuffer );

    The pointer is now invalid and may not be stored for future use, if we wish to modify the object again, we have to call GL.MapBuffer again.

Further reading
Visit this link in order to tell OpenGL about the composition of your Vertex data, and this link for drawing the data.

Optimization:

One hint from the nVidia whitepaper was regarding the situation, if we want to update all data in the buffer object by using GL.MapBuffer and not retrieve any of the old data. Although this is a bad idea, because mapping the buffer is a more expensive operation than just calling GL.BufferData, it might be necessary in cases where you have no copy of the data in system memory, but build it on the fly. The solution to making this somewhat efficient is first calling GL.BufferData with a IntPtr.Zero again, which tells the driver that the old data isn't valid anymore. Calling GL.MapBuffer will return a new pointer to a valid memory location of the requested size to write to, while the old data will be cleaned up once it's not used in any draw operations anymore.

Also note that either reading from a VBO or wrapping it into a Display List is very slow and should both be avoided.

Table 1:
BufferUsageHint.Static... Assumed to be a 1-to-n update-to-draw. Means the data is specified once (during initialization).
BufferUsageHint.Dynamic... Assumed to be a n-to-n update-to-draw. Means the data is drawn multiple times before it changes.
BufferUsageHint.Stream... Assumed to be a 1-to-1 update-to-draw. Means the data is very volatile and will change every frame.

...Draw Means the buffer will be used to sending data to GPU. video memory (Static|StreamDraw) or AGP (DynamicDraw)
...Read Means the data must be easy to access, will most likely be system or AGP memory.
...Copy Means we are about to do some ..Read and ..Draw operations.

3.b Attribute Offsets and Strides

Setting Strides and Offsets for Vertex Arrays and VBO

There are 2 ways to tell OpenGL in which layout the Vertices are stored:

  1. GL.InterleavedArrays()
    What GL.InterleavedArrays does is enable/disable the required client states for OpenGL to interpret our passed data, the first parameter tells that we have 2 floats for Texture Coordinates (T2f), 3 floats for Normal (N3F) and 3 floats for position (V3F). The second parameter is the stride that will be jumped to find the second, third, etc. set of texcoord/normal/position values. Since our Vertices are tighly packed, no stride (zero) is correct. The last parameter should point at Indices, but we already sent them to the VBO in video memory, no need to point at them again:
    GL.InterleavedArrays( InterleavedArrayFormat.T2fN3fV3f, 0, null );

    This command has the advantage that it's very obvious to the OpenGL driver what layout of data we have supplied, and it may be possible for the driver to optimize the memory. Remember that GL.InterleavedArrays will change states, if you manually disable EnableCap.VertexArray, EnableCap.NormalArray, EnableCap.TextureCoordArray or changing GL.VertexPointer, GL.NormalPointer or GL.TexCoordPointer (after calling GL.InterleavedArrays and before calling GL.DrawElements) make sure to enable them again or you won't see anything.
  2. Setting the offsets/strides manually
    For the Vertex format InterleavedArrayFormat.T2fN3fV3f, the correct pointer setup is:
    GL.TexCoordPointer( 2, TexCoordPointerType.Float, 8 * sizeof( float ), (IntPtr) ( 0 ) );
    GL.NormalPointer( NormalPointerType.Float, 8 * sizeof( float ), (IntPtr) ( 2 * sizeof( float ) ) );
    GL.VertexPointer( 3, VertexPointerType.Float, 8 * sizeof( float ), (IntPtr) ( 5 * sizeof( float ) ) );

    1. The first parameter is the number of components to describe that attribute. For GL.NormalPointer this is always 3 components, but is variable for Texture coordinates and Position (and Color).
    2. The second parameter is the type of the components.
    3. The third parameter is the number of bytes between Texture coordinate 0, 1, 2, 3, etc. also known as stride.
    4. The last parameter indicates the byte offset of the first appearance of the attribute, this makes perfect sense if you recall the layout of our Vertex struct.

3.c Vertex Arrays

Although it's really not recommended using them (besides for compiling to a Display List), Vertex Arrays can be safely used like this:

pseudocode:

float[] Positions;
float[] Normals;

unsafe
{
    fixed (float* PositionsPointer = Positions)
    fixed (float* NormalsPointer = Normals)
    {
        GL.NormalPointer(..., NormalsPointer);
        GL.VertexPointer(..., PositionsPointer);
        GL.DrawArrays(...);
        GL.Finish();    // Force OpenGL to finsh drawing while the arrays are still pinned.
    }
}

You must use the unsafe float* overloads for this to work, not the object overloads (which pin internally)

The important bit is that the pin between GL.*Pointer and GL.Draw* may not be released. For arrays smaller than 85000 Bytes it is also important to call GL.Finish in order for the CPU to wait until the GPU is done reading all pinned data. You might get away without the GL.Finish command for a very short VA or ones larger than 85000 Bytes, but once the pin is released the Garbage Collector is allowed to move or cleanup your array. This leads to difficult to trace access violations with medium sized VA (without waiting for OpenGL to finish processing it) and will randomly occur at frames where the GC kicks in.

GL.Finish will obviously kill performance, since you have a blocking statement executed, which forces CPU and GPU to work in sync and not taking advantage of parallel processing. That's why it's recommended to use Display Lists or Vertex Buffer Objects instead.

Please visit this discussion in the forum for more information.

4. Drawing

In order to tell OpenGL to draw primitives for us, there's basically two ways to go:

1. Immediate Mode, as in specifying every single Vertex manually.
2. Vertex Buffers (or Vertex Arrays), drawing a whole Mesh with a single Command.

In order for all GL.Draw*-Functions to output geometric Primitives, EnableCap.VertexArray must be enabled first.

  1. Immediate Mode
    Hereby every single Vertex we wish to draw has to be issued manually.
    // GL.DrawArrays behaviour
    GL.Begin( BeginMode.Points );
    for ( uint i = 0; i < Vertices.Length; i++ )
    {
       GL.TexCoord2( Vertices[ i ].TexCoord );
       GL.Normal3( Vertices[ i ].Normal );
       GL.Vertex3( Vertices[ i ].Position );
    }
    GL.End( );

    // GL.DrawElements behaviour
    GL.Begin( BeginMode.Points );
    for ( uint i = 0; i < Indices.Length; i++ )
    {
       GL.TexCoord2( Vertices[ Indices[ i ] ].TexCoord );
       GL.Normal3( Vertices[ Indices[ i ] ].Normal );
       GL.Vertex3( Vertices[ Indices[ i ] ].Position );
    }
    GL.End( );
  2. GL.DrawArrays( BeginMode, int First, int Length )
    This Command is used together with Vertex Arrays or Vertex Buffer Objects, see setting Attribute Pointers. This line will automatically draw all Vertex contained in Vertices in order of appearance in the Array.
    GL.DrawArrays( BeginMode.Points, 0, Vertices.Length );
  3. GL.DrawElements( BeginMode, int Length, DrawElementsType, object )
    Like DrawArrays this Command is used together with Vertex Arrays or VBO. It uses an unsigned Array (byte, ushort, uint) to Index the Vertex Array. This is particularly useful for 3D Models where the Triangles describing the surface share Edges and Vertices.
    GL.DrawElements( BeginMode.TriangleStrip, Indices.Length, DrawElementsType.UnsignedInt, Indices );
  4. GL.DrawRangeElements( BeginMode, int Start, int End, int Count, DrawElementsType, object )
    This function behaves largely like GL.DrawElements, with the change that you may specify a starting index rather than starting at 0.
    Start and End are the smallest and largest Array-Index into Indices. Count is the number of Elements to render.
    // behaviour equal to GL.DrawElements
    GL.DrawRangeElements( BeginMode.TriangleStrip, 0, Indices.Length-1, Indices.Length, DrawElementsType.UnsignedInt, Indices );
  5. GL.DrawArraysInstanced( BeginMode, int First, int Length, int primcount )
    This function is only available on DX10 hardware and basically behaves like this:
    for (int gl_InstanceID=0; gl_InstanceID < primcount; gl_InstanceID++ )
       GL.DrawArrays( BeginMode, First, Length );

    gl_InstanceID is a uniform variable available to the Vertex Shader, which (in conjunction with GL.UniformMatrix) can be used to assign each Instance drawn it's own unique Orientation Matrix.
  6. GL.DrawElementsInstanced( BeginMode, int Length, DrawElementsType, object, int primcount )
    This function is only available on DX10 hardware and internally unrolls into:
    for (int gl_InstanceID=0; gl_InstanceID < primcount; gl_InstanceID++ )
       GL.DrawElements( BeginMode, Length, DrawElementsType, Object );

    gl_InstanceID is a uniform variable available to the Vertex Shader, which (in conjunction with GL.UniformMatrix) can be used to assign each Instance drawn it's own unique orientation Matrix.

Extension References
http://www.opengl.org/registry/specs/EXT/draw_range_elements.txt
http://www.opengl.org/registry/specs/EXT/draw_instanced.txt
http://www.opengl.org/registry/specs/EXT/multi_draw_arrays.txt

4.b Drawing Optimizations

This page is just giving a starting point for optimizations, the links below provide more in-depth information.

  • Make sure there are no OpenGL errors. Any error usually kills the framerate.
  • Enable backface culling with GL.Enable(EnableCap.CullFace) rather than relying on the Z-Buffer.
  • If you're drawing a scene that covers the whole screen each frame, only clear depth buffer but not the color buffer.
  • Organize drawing in a way that requires as few state changes as possible. Don't enable states that are not needed.
  • Use GL.Hint(...) wherever applicable.
  • Avoid Immediate Mode or Vertex Arrays in favor of Display Lists or Vertex Buffer Objects.
  • Use GL.DrawRangeElements instead of GL.DrawElements for a slight performance gain.
  • Take advantage of S3TC Texture compression and Vertex Cache optimizing algorithms.

Links:

http://www.mesa3d.org/brianp/sig97/perfopt.htm
http://ati.amd.com/developer/SDK/AMD_SDK_Samples_May2007/Documentations/...
http://developer.nvidia.com/object/gpu_programming_guide.html
http://www.opengl.org/pipeline/article/vol003_8/
http://developer.apple.com/graphicsimaging/opengl/

Last edit of Links: March 2008

5. OpenTK.Utilities standard Objects and Model Loader

Placeholder

this page will contain whatever spawns off http://opentk.sourceforge.net/home/?q=node/58

Avoiding pitfalls in the managed world

This text is intended for someone with a C/OpenGL background.

Even though OpenTK automatically translates GL/AL calls from C# to C, some things work slightly differently in the managed world, when compared to plain C. This page describes a few rules you need to keep in mind:

Rules of thumb

  1. Use server storage rather than client storage.
    A few legacy OpenGL functions use pointers to memory managed by the user. The most popular example is Vertex Arrays, with the GL.***Pointer family of functions.

    This approach cannot be used in a Garbage Collected environment (as .NET), as the garbage collector (GC) may move the contents of the buffer in memory. It is strongly recommended that you replace legacy Vertex Arrays with Vertex Buffer Objects, which do not suffer from this problem.

    Unlike OpenGL 2.1, OpenGL 3.0 will not contain any functions with client storage.

  2. Try to minimize the number of OpenGL calls per frame.
    This is true for any programming environment utilizing OpenGL, but a little more important in the OpenTK case; while the OpenGL/OpenAL bindings are quite optimized, the transition from managed into unmanaged code incurs a small, but measurable, overhead.

    To minimize the impact of this overhead, try to minimize the number of OpenGL/OpenAL calls. A good rule of thumb is to make no more than 300-500 OpenGL calls per frame, which can be achieved by avoiding Immediate Mode, in favour of Display Lists and VBO's.

  3. For optimal math routine performance, use the ref overloads.
    This is because Vector3, Matrix3 etc. are structures, not classes. Classes are passed by reference by default in C#.
    Vector3 v1 = Vector3.UnitX;
    Vector3 v2 = Vector3.UnitZ;
    Vector3 v3 = Vector3.Zero;
    v3 = v1 + v2;                        // requires three copies; slow.
    Vector3.Add(ref