Miyabiarts.net

一年に一度の更新頻度

カテゴリーアーカイブ: OpenTK

FBO

今回はFBO(Frame Buffer Object)を扱います。
FBOが何かと一言で言ってしまえば、ディスプレイではなくテクスチャ上にレンダリング結果を書き出すためのものです。
ハードウェアの拡張機能の一つに入るため、グラフィックボードによっては使えない場合がありますが、最近の環境ではまず間違いなく動くでしょうから、それほど心配する必要はないはずです。

今回、サンプルとして一度FBOにテクスチャ付きのキューブをレンダリングし、そのレンダリング結果をテクスチャとして再度キューブに貼りつけたものをディスプレイ上に表示します。

FBOはFBOハンドルに対して、RGBテクスチャおよび深度テクスチャを関連付けることによって有効にします。

テクスチャの作成

各テクスチャの作り方は以下のとおりです。

RGBテクスチャ

RGBテクスチャですが、こちらは前回と全く同じように作成して大丈夫です。
ここで、TexSizeはテクスチャのサイズを示す定数で、今回はテクスチャは正方形になるように作成しています。

GL.GenTextures(1, out colorTex);
GL.BindTexture(TextureTarget.Texture2D, colorTex);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, TexSize, TexSize, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, IntPtr.Zero);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);

深度テクスチャ

続いて、深度テクスチャですが、作り方の流れはRGBテクスチャと全く一緒ですが、GL.TexImage2Dに指定するパラメータが多少違います。

GL.GenTextures(1, out depthTex);
GL.BindTexture(TextureTarget.Texture2D, depthTex);
GL.TexImage2D(TextureTarget.Texture2D, 0, (PixelInternalFormat)All.DepthComponent32, TexSize, TexSize, 0, OpenTK.Graphics.OpenGL.PixelFormat.DepthComponent, PixelType.UnsignedInt, IntPtr.Zero);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);

FBOの作成

FBOハンドルはGL.Ext.GetFramebuffersを用いて、他のハンドラと同じように作成できます。
そして、GL.Ext.BindFramebufferに作成したハンドラを設定し、操作対象とします。
続いて、先ほど作成したRGBテクスチャ・深度テクスチャをFBOに関連付けます。
以上で、FBOの準備は整いました。
ここで気をつけることは、GL.Ext.BindFramebufferで指定したFBOをそれ以降のレンダリング対象となるため、初期化の後にFBOを0と設定することで、ディスプレイをレンダリング対象としておきましょう。
これによって、何も表示されないということを防ぐことができます。

GL.Ext.GenFramebuffers(1, out fbo);
GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, fbo);
GL.Ext.FramebufferTexture2D(FramebufferTarget.FramebufferExt, FramebufferAttachment.ColorAttachment0Ext, TextureTarget.Texture2D, colorTex, 0);
GL.Ext.FramebufferTexture2D(FramebufferTarget.FramebufferExt, FramebufferAttachment.DepthAttachmentExt, TextureTarget.Texture2D, depthTex, 0);
GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0);

レンダリング

GL.Ext.BindFramebufferでレンダリング対象とするFBOを指定することで、描画先を切り替えることができます。
今回は、まず作成したFBOに対して、テクスチャ付きのキューブをレンダリングします。
用いたシェーダは前回と全く同じものです。

GL.Ext.BindFramebufferでFBOを指定した後、気をつける点としては、クリアする領域をFBOに関連付けたテクスチャのサイズとするため、GL.Viewportを正しく設定してやることです。
その他の点に関して、通常のレンダリングと何も変わりません。

GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, fbo);
GL.Viewport(0, 0, TexSize, TexSize);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

...

GL.UseProgram(program);

GL.UniformMatrix4(GL.GetUniformLocation(program, "viewProjection"), false, ref viewProjectionMatrix);
GL.UniformMatrix4(GL.GetUniformLocation(program, "world"), false, ref worldMatrix);
			
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, tex);
GL.Uniform1(GL.GetUniformLocation(program, "tex"), 0);
			
cube.Render();

今度は、FBOにレンダリングした結果をテクスチャとして貼りつけたキューブをディスプレイ上に表示します。
ディスプレイ上に表示するため、GL.Ext.BindFramebufferに0を与えます。
そして、今度はクリアする領域をディスプレイ(コントロール)のサイズとするようにGL.Viewportを正しく設定します。
最後に、FBOに関連付けていたテクスチャハンドラを貼り付けるように設定し、キューブを表示します。

GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0);
GL.Viewport(0, 0, glControl1.Width, glControl1.Height);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

...

GL.UniformMatrix4(GL.GetUniformLocation(program, "viewProjection"), false, ref viewProjectionMatrix);
GL.UniformMatrix4(GL.GetUniformLocation(program, "world"), false, ref worldMatrix);

GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, colorTex);
GL.Uniform1(GL.GetUniformLocation(program, "tex"), 0);

cube.Render();

後片付け

後片付けでは、各ハンドラを削除します。

GL.DeleteTexture(tex);
GL.DeleteTexture(colorTex);
GL.DeleteTexture(tex);

GL.DeleteFramebuffers(1, ref fbo);

まとめ

以上が、OpenTKでFBOを扱うサンプルコードとなります。
今回のサンプルでは実際に何に使うかはイマイチ分かりづらいですが、環境マップやシャドウマップなどの応用は様々です。
そのうち、このあたりの応用も扱いたいと思います。

テクスチャ表示

今回はテクスチャを張ります。
今回もコードはgithubにあります。
これからは、このシリーズは1つのソリューション内に別のプロジェクトとして追加していくことにしました。

テクスチャの準備

はじめにテクスチャを管理するためのハンドラをGL.GenTexturesで作成します。
次に、そのテクスチャに対する設定を行うため、GL.BindTextureで設定対象とします。
GL.TexParameterで色々設定することができます。
とりあえずここでは、縮小・拡大するときに補間方法を線形フィルタに指定しています。

GL.GenTextures(1, out tex);
GL.BindTexture(TextureTarget.Texture2D, tex);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);

テクスチャの中身を作るにはGL.TexImage2Dを使います。
以下の例では、512×512の32ビットRGBA画像を作っています。

int texSize = 512;
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, texSize , texSize , 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, IntPtr.Zero);

通常は、ファイルから画像を読み込んでから表示したいでしょうから、その場合は以下のように画像を読み込んで、その画像データをテクスチャに転送してやります。

Bitmap bitmap = new Bitmap("texture.png");
BitmapData data = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);
bitmap.UnlockBits(data);

頂点形式

今回はテクスチャを張った平面を表示するためPlaneクラスを作ります。
各頂点にはテクスチャを参照するUV座標が必要となるため、以下のように頂点座標・UV座標を持った頂点形式を用います。
頂点バッファを作成する処理などは前回と同様です。

class Plane
{
  struct Vertex
  {
    public Vector3 position;
    public Vector2 texcoord;

    public Vertex(Vector3 position, Vector2 texcoord)
    {
      this.position = position;
      this.texcoord = texcoord;
    }
    public static readonly int Stride = Marshal.SizeOf(default(Vertex));
  }
  ...
}

シェーダ

頂点シェーダ(textured.vert)

頂点シェーダには、頂点形式と同様に頂点座標とUV座標をattribute変数として与えます。
また、フラグメントシェーダに対してUV座標を渡す、varying変数を用意しておきます。
uniform変数に対する値の設定方法は前回と同様です。

attribute vec3 position;
attribute vec2 texcoord;
uniform mat4 world;
uniform mat4 viewProjection;
varying vec2 uv;
void main(void)
{
	gl_Position = viewProjection * world * vec4(position, 1.0);
	uv = texcoord;
}

フラグメントシェーダ(textured.frag)

フラグメントシェーダでは、sampler2D型のuniform変数を定義することでテクスチャにアクセスします。
sampler2D型は名前が示していますが、テクスチャそのものではなく、テクスチャとそれからのサンプリング方法がセットになっている型だと考えてください。
そして、texture2D関数にsampler2DとUV座標を渡すことによって、テクスチャから色情報を取得することができます。
sampler2D型のuniform変数の設定方法は次で説明します。

uniform sampler2D tex;
varying vec2 uv;
void main(void)
{	
	gl_FragColor = texture2D(tex,uv);
}

テクスチャの設定

フラグメントシェーダが持つsampler2D型のuniform変数は、前回のようにテクスチャのハンドラを直接設定することではできません。
以下のように、一度テクスチャユニットに対して、テクスチャのハンドラを設定した後で、uniform変数に対してそのテクスチャユニットのインデックスを与えます。
テクスチャユニットは、GL.Uniform1を使って1次元の値として設定します。

あとは、正しい頂点形式を持つオブジェクトをレンダリングしてやれば、テクスチャが上手く表示されます。

GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, tex);
GL.Uniform1(GL.GetUniformLocation(program, "tex"), 0);

plane.Render();

後処理

最後にテクスチャの破棄をしましょう。

GL.DeleteTexture(1,ref tex);

OpenTKでキューブを表示

OpenTKの紹介をするだけなのもなんだったので、簡単なサンプルコードも載せることにしました。
OpenGLもバージョン3以降になると、glBegin/glEndやglVertex*といった固定機能が使えなくなり、シェーダやVBOを使ってレンダリングすることが当たり前になっているので、今回はそちらに合わせるようにしています。

コードの方は、githubに置くようにしました。
リポジトリ:https://github.com/miyabiarts/CubeView

準備

最初は、ソリューションを作成した後に参照として、「OpenTK」「OpenTK.GLControl」を加えます。
これでOpenTKの機能、およびOpenGLのレンダリング領域を使う準備ができました。

GLControlの配置

OpenTKにおけるOpenGLのレンダリング領域は、ユーザコントロールGLControlを親ウィンドウ上に配置すれば良いです。
上の図が配置した結果で、GLControlのDockプロパティをFillにすることで、親ウィンドウのサイズでレンダリング出来るようにします。

メインループ

定期的にレンダリングするために、元のProgram.cs内のMain関数を以下のように書き換えます。

static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);

  MainForm form = new MainForm();
  form.Show();
  while (form.Created)
  {
    form.Render();
    Application.DoEvents();
}

MainFormはGLControlを貼りつけたフォームで、定期的にレンダリングするためのメソッドRenderを定義しておきます。
他にもタイマーを使った呼び出し方などがありますが、一般的に良く使われる方法を使いました。

シェーダの読み込み

最近のOpenGLでは固定機能を使わずに、シェーダを使ってレンダリングするのが必須になり始めています。
そのため、自分でシェーダを用意し、レンダリングする前に準備する必要があります。
今回は、簡単なPhongシェーダで、拡散反射のみを扱います。
シェーダは、頂点シェーダとフラグメントシェーダのみを扱い、リソースとしてソリューションに含んでおきます。
なお、ジオメトリシェーダについては今回触れません。
各シェーダを以下に示します。

頂点シェーダ(phong.vert)

attribute vec3 position;
attribute vec3 normal;
uniform mat4 world;
uniform mat4 viewProjection;
uniform vec3 lightDir;
varying vec4 diffuseColor;
void main(void)
{
	gl_Position = viewProjection * world * vec4(position, 1.0);
	diffuseColor = vec4(max(0,dot(mat3(world) * normal,-lightDir)));
	diffuseColor.a = 1.0;
}

フラグメントシェーダ(phong.frag)

varying vec4 diffuseColor;
void main(void)
{
	gl_FragColor = diffuseColor;
}

シェーダについての詳細については、今回は説明を省きます。
シェーダのバージョンによっては、上のシェーダがそのまま動かない場合もありますので、その場合は適宜変更してください。
また、VisualStudioでシェーダを作成した場合は、BOM付きUTF-8のテキストファイルになるのですが、そのまま読み込むとBOMでエラーを起こすので、必ずBOM無しUTF-8などで保存してください。

以上の頂点シェーダ・フラグメントシェーダを読み込んで、int型で表されるprogramで管理します。
テストコード内では、CreateShaderでリソースから読み込んだ頂点シェーダ・フラグメントシェーダからprogramを作成しています。

int CreateShader(string vertexShaderCode, string fragmentShaderCode)
{
  int vshader = GL.CreateShader(ShaderType.VertexShader);
  int fshader = GL.CreateShader(ShaderType.FragmentShader);

  string info;
  int status_code;

  // Vertex shader
  GL.ShaderSource(vshader, vertexShaderCode);
  GL.CompileShader(vshader);
  GL.GetShaderInfoLog(vshader, out info);
  GL.GetShader(vshader, ShaderParameter.CompileStatus, out status_code);
  if (status_code != 1)
  {
    throw new ApplicationException(info);
  }

   // Fragment shader
  GL.ShaderSource(fshader, fragmentShaderCode);
  GL.CompileShader(fshader);
  GL.GetShaderInfoLog(fshader, out info);
  GL.GetShader(fshader, ShaderParameter.CompileStatus, out status_code);
  if (status_code != 1)
  {
    throw new ApplicationException(info);
  }

  int program = GL.CreateProgram();
  GL.AttachShader(program, vshader);
  GL.AttachShader(program, fshader);

  GL.LinkProgram(program);

  return program;
}

やっていることは、頂点シェーダ・フラグメントシェーダの順に文字列をコンパイルしてから、programに割り当てています。
シェーダにエラーが発見されれば、例外が飛びます。

キューブの作成・レンダリング

以前は、glBegin/glEnd/glVertex*あたりを使えば、とりあえず簡単にオブジェクトをレンダリングできたのですが、最近のOpenGLではVBOを作る必要があり、結構面倒くさいです。
面倒くさいですが、これからそうなっていくので、あまり嫌がらずやり方を覚えていきましょう。
今回はキューブを表示するため、Cubeクラスを定義します。

頂点形式

はじめに、頂点、および頂点インデックスから頂点バッファを作成します。
このとき、頂点の形式は用いるシェーダと同じものである必要があります。
このあたりは、DirectX(10以降)でもだいたい同じです。
今回用いるシェーダでは、頂点ではその位置と法線を用いていますため、Cubeクラス内で以下のようなVertex構造体を定義しておきます。

struct Vertex
{
  public Vector3 position;
  public Vector3 normal;

  public Vertex(Vector3 position, Vector3 normal)
  {
    this.position = position;
    this.normal = normal;
  }
  public static readonly int Stride = Marshal.SizeOf(default(Vertex));
}

データ定義

キューブの頂点、および頂点インデックスはコード内に静的に埋め込んでいます。
今回は1つの頂点につき1つの法線を割り当てるようにし、各頂点に接する面の法線の平均を取った形になります。
下のコードでは、方向を与えた後に大きさを正規化しています。
頂点インデックスは、この頂点へのインデックスの集合で、三角面リストとして与えています。

public Cube(int program)
{
  Vertex[] v = new Vertex[]{
      new Vertex(new Vector3(-1.0f, 1.0f,-1.0f), new Vector3(-1.0f, 1.0f,-1.0f)),
      new Vertex(new Vector3( 1.0f, 1.0f,-1.0f), new Vector3( 1.0f, 1.0f,-1.0f)),
      new Vertex(new Vector3( 1.0f, 1.0f, 1.0f), new Vector3( 1.0f, 1.0f, 1.0f)),
      new Vertex(new Vector3(-1.0f, 1.0f, 1.0f), new Vector3(-1.0f, 1.0f, 1.0f)),
      new Vertex(new Vector3(-1.0f,-1.0f,-1.0f), new Vector3(-1.0f,-1.0f,-1.0f)),
      new Vertex(new Vector3( 1.0f,-1.0f,-1.0f), new Vector3( 1.0f,-1.0f,-1.0f)),
      new Vertex(new Vector3( 1.0f,-1.0f, 1.0f), new Vector3( 1.0f,-1.0f, 1.0f)),
      new Vertex(new Vector3(-1.0f,-1.0f, 1.0f), new Vector3(-1.0f,-1.0f, 1.0f)),
  };
  foreach(Vertex vv in v )
  {
    vv.normal.Normalize();
  }

  uint[] indices = new uint[]{
      0,1,2,
      0,2,3,
      1,0,4,
      1,4,5,
      2,1,5,
      2,5,6,
      3,2,6,
      3,6,7,
      0,3,7,
      0,7,4,
      6,5,4,
      7,6,4
  };

頂点バッファの作成

以上のデータからVBO(頂点バッファ)、EBO(頂点インデックスバッファ)を作ります。
作り方は簡単で、GL.GenBuffersでハンドラを作成し、BindBufferで操作対象とし、GL.BufferDataで上のデータを書きこむだけです。

  // VBO作成
  GL.GenBuffers(1, out vbo);
  GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
  GL.BufferData<Vertex>(BufferTarget.ArrayBuffer, new IntPtr(v.Length * Vertex.Stride), v, BufferUsageHint.StaticDraw);

  // EBO作成
  GL.GenBuffers(1, out ebo);
  GL.BindBuffer(BufferTarget.ElementArrayBuffer, ebo);
  GL.BufferData<uint>(BufferTarget.ElementArrayBuffer, new IntPtr(sizeof(uint) * indices.Length), indices, BufferUsageHint.StaticDraw);
  vertexCount = indices.Length;

シェーダとの関連付け

レンダリングするためのデータは準備できましたが、まだ頂点のどの要素がシェーダのどの変数に対応するかが分からないため、レンダリングができない状態です。
そのため、VAO(Vertex Array Object)を定義してやり、シェーダとの頂点のやりとりをできるようにしてやります。
まずはVAOのハンドラを作成し、有効にします。
また、関連付けるVBOも有効にします。
次に、シェーダから関連付ける頂点要素のシェーダ内におけるインデックスをGL.GetAttribLocationで取得します。
この時取得するのは、シェーダ内でattributeで定義されている変数で、取得にはその変数名を用います。
続いて、取得し頂点要素を有効にします。
最後に各要素がVBOをどのように参照すれば良いかをGL.VertexAttribPointerで設定します。
GL.VertexAttribPointerでは、頂点要素のインデックス・頂点要素を構成する型(floatなど)の数・頂点要素の型・頂点要素を正規化するか・同じ頂点要素間のバイト数・頂点要素のバイト数をパラメータとして与えます。
頂点要素の正規化は、法線や色の場合にtrueにします。
また、同じ頂点要素間のバイト数は頂点全体のバイト数を与えれば良く、頂点要素のバイト数は頂点が定義されている順番にしたがって与えてやる必要があります。
このあたりは、頂点が定義される順番を把握していなければならないため、多少煩雑に感じるかもしれません。

  // VAO作成
  GL.GenVertexArrays(1, out vao);
  GL.BindVertexArray(vao);

  GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);

  // シェーダ内の頂点要素のインデックスを取得
  int positionLocation = GL.GetAttribLocation(program, "position");
  int normalLocation = GL.GetAttribLocation(program, "normal");

  // 頂点要素を有効にする
  GL.EnableVertexAttribArray(positionLocation);
  GL.EnableVertexAttribArray(normalLocation);

  // 頂点要素のサイズ・オフセットを設定
  GL.VertexAttribPointer(positionLocation, 3, VertexAttribPointerType.Float, false, Vertex.Stride, 0);
  GL.VertexAttribPointer(normalLocation, 3, VertexAttribPointerType.Float, true, Vertex.Stride, Vector3.SizeInBytes);
}

レンダリング

レンダリングは非常に簡単で、作成したVBO・EBOを設定し、GL.DrawElementsで呼び出すだけです。
GL.DrawElementsでは、レンダリングするプリミティブの数ではなく、含まれる全頂点数を与えてやる必要があるため、頂点バッファを作成する段階で頂点インデックスに含まれる頂点数を事前に保存しておくと楽です。

public void Render()
{
  GL.BindVertexArray(vao);
  GL.BindBuffer(BufferTarget.ElementArrayBuffer, ebo);
  GL.DrawElements(BeginMode.Triangles, vertexCount, DrawElementsType.UnsignedInt, 0);
}

シェーダへのパラメータ設定・レンダリング

今回は、GLControlをviewportという変数で用いています。
OpenTKのOpenGLでは、GLControlのMakeCurrentでレンダリング対象を設定し、レンダリングを行った最後にSwapBuffersを呼び出すことで画面へ出力します。
ここでは、シェーダで定義したパラメータに対して、実際の値を設定することで、レンダリングを行います。

下記のコードで、いくつかはOpenGLで良く使うものなので、シェーダまわりの説明をします。
まず、GL.UseProgramで使用したいシェーダを指定します。
シェーダは既に作成したprogramによって与えます。
今回シェーダが必要としているパラメータは、uniform変数で定義されたビュー行列・プロジェクション行列(をかけ合わせた行列)、ワールド行列、ライトの方向となります。
ビュー行列・プロジェクション行列は、OpenTKが持つ数学ライブラリを用いることによって簡単に求めることができます。
また、ワールド行列は単位行列にしています。(全くもって意味ないパラメータです。)
ライトの方向に関しては、動きがないと良く分からないのでキューブの中心に向かって回転するようにしました。

シェーダ内のuniform変数へのアクセスは、その型に応じてGL.Uniform*で与えてやります。
行列ならばGL.UniformMatrix4、3次元ベクトルならばGL.Uniform3といった感じです。
これらの第1引数として、対応するuniform変数のシェーダ内のインデックスが必要となりますので、GL.GetUniformLocationに変数名を指定することで取得できます。
今回は、Renderが呼び出されるたびに取得するようにしていますが、シェーダごとにHogeShaderクラスを作ってラッパしたほうがコードが綺麗になると思います。

全てのシェーダパラメータを設定したら、キューブのレンダリングを行いましょう。

public void Render()
{
  // ビューポート(GLControl)を有効にする
  viewport.MakeCurrent();

  // 各種設定
  GL.Enable(EnableCap.DepthTest);
  GL.Enable(EnableCap.CullFace);
  GL.FrontFace(FrontFaceDirection.Cw);
  GL.CullFace(CullFaceMode.Back);

  // レンダリング領域をクリア
  GL.Viewport(0, 0, viewport.Width, viewport.Height);
  GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

  // シェーダを設定
  GL.UseProgram(program);

  // ビュー・プロジェクション行列を設定
  Vector3 eyePos = new Vector3(5.0f, 5.0f, 5.0f);
  Vector3 lookAt = new Vector3(0.0f, 0.0f, 0.0f);
  Vector3 eyeUp = new Vector3(0.0f, 1.0f, 0.0f);
  Matrix4 viewMatrix = Matrix4.LookAt(eyePos, lookAt, eyeUp);
  Matrix4 projectionMatrix = Matrix4.CreatePerspectiveFieldOfView((float)System.Math.PI / 4.0f, (float)viewport.Width / (float)viewport.Height, 0.1f, 10.0f);
  Matrix4 viewProjectionMatrix = viewMatrix * projectionMatrix;
  GL.UniformMatrix4(GL.GetUniformLocation(program, "viewProjection"), false, ref viewProjectionMatrix);

  // ワールド行列を設定
  Matrix4 worldMatrix = Matrix4.Identity;
  GL.UniformMatrix4(GL.GetUniformLocation(program, "world"), false, ref worldMatrix);

  // ライトを設定
  Vector3 lightDir = new Vector3((float)Math.Cos((float)lightAngle), -1.0f, (float)Math.Sin((float)lightAngle));
  lightDir.Normalize();
  GL.Uniform3(GL.GetUniformLocation(program, "lightDir"), lightDir);
  lightAngle += 0.001f;

  // Cubeをレンダリング
  cube.Render();

  // バッファをスワップして画面表示
  viewport.SwapBuffers();
}

まとめ

実際コーディングしてみるとさほど難しいというわけではないですが、まとまった分かりやすい資料があまりないため、難しく感じてしまうかもしれません。
特に固定機能の廃止は、とりあえず絵を出してみたい人にとっては、余計に小難しくしている感があります。
とりあえず最初は写経でも良いので、絵が出るところまで頑張ってみることをお勧めします。
そのための助けになるならば幸いです。

ちなみにですが、オブジェクトの削除関連を思いっきりサボっています。すみません。

OpenTKのインストール

OpenTKは、C#などからOpenGLやOpenALなどを扱うためのライブラリです。
Vector3などの数学ライブラリが付いていたり、ゲームパッド入力なども入っており、Monoでも動くことを考慮するC++のSDLみたいな位置付けと考えても良さそうです。

インストール方法

インストール方法は、公式ページからインストーラをダウンロードして実行するだけですが、一部数学ライブラリに問題があるため、Subversionのtrunkをチェックアウトして、自分でビルドする方が良いと思います。
trunkのURLは以下となっています。

https://opentk.svn.sourceforge.net/svnroot/opentk/trunk

チェックアウトしたら、「OpenTK.sln」を開いて、DebugかReleaseでビルドすると、さほど時間がかかることもなくライブラリが作成されます。
作成されたライブラリは、チェックアウトしたディレクトリ以下の「OpenTK/Binaries/Debug or Release」にある「OpenTK.dll」「OpenTK.GLControl.dll」が必要となります。

使いかた

まずは、「参照設定」で先程の「OpenTK.dll」「OpenTK.GLControl.dll」を追加しましょう。
ここでは、OpenTKの中のOpenGL周りの使いかたについて簡単に説明します。
OpenALやInputについても、usingを増やすだけなので特に難しいことはありません。
usingに関しては、基本的に以下の3つを使えば良いです。

using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;

OpenGLのレンダリング領域には、GLControlというユーザコントロールを用います。
「OpenTK.GLControl.dll」を追加しておくと、デザイナにGLControlが現れているので、それを親ウィンドウ上に貼り付けるだけで良いです。
そして、Paintイベントや、TImerイベントなどでOpenGLのレンダリング命令を呼び出します。

OpenTKのOpenGLでは、元々の「glHoge」というAPIが「GL.Hoge」として使えるようになります。
例えば、直線を書きたい場合は、以下のような感じです。

GL.Begin(BeginMode.Lines);
GL.Vertex3(0.0f,0.0f,0.0f);
GL.Vertex3(1.0f,1.0f,1.0f);
Gl.End();

実際にはこのあたりの固定機能の命令はOpenGL4.1ではなくなっているので、VBOやVSAを使ってシェーダでレンダリングするのがモダンな使いかただと思います。
また、「using OpenTK.Graphics.OpenGL」の代わりに「using OpenTK.Graphics.ES20」を使うとOpenGL ES2.0を使えたりもします。

基本的にはOpenGLのAPIをそのままラッパしたものなので、今までOpenGLを使ったことがある方は簡単に移行できるかと思います。