Miyabiarts.net

一年に一度の更新頻度

月別アーカイブ: 7月 2011

第9回 名古屋CV・PRML勉強会

今回は、コンピュータビジョンとパターン認識の国際会議CVPR2011で発表された論文の紹介をテーマに開催しました。
全部で以下の13件を紹介しました。
(発表資料は現在取りまとめ中で、後ほど公開する予定です。)
なお、全てではないですが、元の論文はこちらで見ることができます。
  • [Best Paper] Real-time Human Pose Recognition in Parts from Single Depth Images, Jamie Shotton, Andrew Fitzgibbon, Mat Cook, Andrew Blake
    • Kinectから得られる距離画像を用いて、自分物の姿勢を認識する研究。認識にはRandom Forestsを利用し、学習には人工的に生成した3次元データを利用。
  • Single Image Super-Resolution using Gaussian Process Regression, He He, Wan-Chi Siu.
    • 1枚のみの画像を用いた超解像。元の画像をダウンサンプリングした画像間との関係を回帰し、それを超解像に利用。
  • Space-Time Super-Resolution from a Single Video, Oded Shahar, Alon Faktor, Michal Irani.
    • 空間方向・時間方向の両方への超解像。動画像から時空間のパッチを探索して、ダウンサンプリングしたパッチとの関係を用いて超解像。時間方向の超解像を用いることでフレームレートを2倍にするなども可能。
  • Enforcing topological constraints in random field image segmentation, Chao Chen, Daniel Freedman, Christoph Lampert
    •  画像中の物体のトポロジーを用いることによって、単純なGraphCutなどに比べて、より精度よいセグメンテーションを達成。
  • Natural Image Denoising: Optimality and Inherent Bounds, Anat Levin, Boaz Nadler.
    • ノイズ除去が理論的にどこまでできるのかを調査する研究。
  • Glare Encoding of High Dynamic Range Images, Mushfiqur Rouf, Rafal Mantiuk, Wolfgang Heidrich, Matthew Trentacoste, Cheryl Lau
    •  撮影するときにクロスフィルターを用いることによって、画像にパターンを埋め込み、その情報を利用することで1枚の画像からHDR画像を生成。
  • A Coarse-to-fine approach for fast deformable object detection, Marco Pedersoli, Andrea Vedaldi, Jordi Gonzàlez
    • Deformable Parts Modelを用いることによって、変形する対象の検出を高精度化。識別関数が非凸関数になるためLatent SVMを利用。高速に検出するためにCorase-to-fineな探索を工夫。
  • 2D Nonrigid Partial Shape Matching Using MCMC and Contour Subdivision, Yu Cao, Zhiqi Zhang, Irina Czogiel, Ian Dryden, Song Wang
    •  輪郭が自己遮蔽などで一部が欠けている物体とテンプレートをうまくマッチングする研究。
  • Monocular 3D Scene Understanding with Explicit Occlusion Reasoning, Christian Wojek, Stefan Walk, Stefan Roth, Bernt Schiele
    • 単一画像から人物などを検出する際に3次元情報を利用することで、検出率を向上させる研究。検出枠の重なりから信頼度を求めることで、人同士が重なっているような状況に対処。群衆シーンにおける人物検出問題などに適用。
  • [Best Student Paper] Recognition Using Visual Phrases, Ali Farhadi, Mohammad Amin Sadegh
    •  Bag of Visual Wordsを発展させたBag of Visual Bilingual Wordsを利用することで、対象とする枠内に人+馬などの複数の物体が含まれる場合にでも適切に認識が可能。
  • Cross-View Action Recognition via View Knowledge Transfer, Jingen Liu
    • 転移学習を利用することによって、異なる視点の画像群を用いた行動の認識を行う研究。
  • [Best Paper Honorable Mention] Discrete-Continuous Optimization for Large-scale Structure from Motion, David Crandall, Andrew Owens, Noah Snavely, Daniel Huttenlocher
    • インターネットに存在する同一地点の画像を収集し、それらの画像を用いることによって3次元復元を行う研究。計算量を大幅に削減しつつ、高精度に復元が可能。
  • Markerless Motion Capture of Interacting Characters Using Multi-view Image Segmentation, Yebin Liu, Carsten Stoll, Juergen Gall, Hans-Peter Seidel, Christian Theobalt
    • 人物の形状の事前確率と色情報を組み合わせることによって、セグメンテーションの精度を向上し、それをマーカなしのモーションキャプチャに利用する研究。
数も多く、時間もいっぱいいっぱいまで使ったので、全部を理解することは難しかったですが、興味を持たせる目的としては良かったです。
とりあえず今回で特に気になったキーワードとしては、
  • Latent SVM
  • Deformable Parts Model
  • Knowledge Transfer
です。
このあたりは考え方も面白く、色々なことに応用でき、これからも流行りそうな手法なので、きちんと勉強したいと思います。
次回は、8月はお休みして9月に行う予定です。
いつも通り、何をするかは未定なので、8月中にまた面白そうなことを考えましょう。

Google+で遊んでみた

Qt勉強会で「まだ招待されていません( ー`дー´)キリッ」と言っていたら、その日のうちに招待されたので、色々と遊んでいました。
とは言ってもまだやれることは少ないのですが。とりあえず、まわりの人達に紹介状を送りつけて、50人くらいのサークルを作成してみました。短縮アドレスを取得したので、私のページにはhttp://gplus.to/miyabiarts/で飛べます。

現状でどんなものかというとTwitterとFacebookの中間くらいのもので、ストリームと呼ばれるTwitterのタイムライン相当のものが画面の中心に大きくあり、サークルと呼ばれるこれまたTwitterのリスト相当のもので、スクリーンに表示するユーザを振り分けます。サークルごとにスクリーンが切り替えられるので、特定の人たちのグループを作って、そのポストを閲覧できるという仕組みです。

Twitterのリストだと、つぶやきを見たい人のタイムラインを作りますが、Google+だと誰に発信したいかということも制御することができるため、うまく機能すれば特定のグループとの情報共有により、議論を活発に行えるかもしれません。あくまでうまく機能すればですが。

使っていて思ったことをいくつか羅列しておきます。

  • UIが見やすい
  • 画像アップロードやGPSに基づくタグ付けなどが便利
  • iGoogleのように情報集約できるようになると嬉しい
  • UIのカスタマイズを色々したい
  • 個人のプロフィールページのカスタマイズ(GoogleSiteの機能を利用)
  • 外部アプリとの連携
  • Webアプリの作成(GoogleAppsと統合)
  • 特定のサークルごとに色を割り当てて、選択しているサークルの色でストリームを表示

どのくらいの頻度で更新されていくかは分かりませんが、気がついたことがあったら意見・要望としてフィードバックにガンガン投げるようにしています。

現状での自分の使い方としては、ブログほどまとめるほどのものでもないけど、Twitterだと収めきれない自分の意見を述べるようにしたいと思います。こちらのブログは基本としては技術ブログという立ち位置で。

最後に、招待して欲しいという方がいれば、ご連絡ください。

Qt名古屋勉強会

関西ゲームプログラミング勉強会の時に補足されたようで、そこからお話があって、発表させていただくことになりました。
名古屋ではQt勉強会は初ということで、結構注目されていたようです。

  • 日付:2011年7月9日(土) 13:00 – 17:30
  • 場所: 株式会社 エイチーム セミナールーム1 (名古屋市西区牛島町6番1号 名古屋ルーセントタワー32F)
  • イベントページ:http://atnd.org/events/16926

元々は物理エンジンBulletについて話すことでしたが、そのままだとQt全く関係なかったりしますし、普段3Dを触ってない方には分からない点も多かろうと、丁度良く出ていたQtの3D周りを扱うQt/3Dとともに紹介することにしました。
Qt/3Dのインストール方法などは前の記事で書いています。

発表資料はこちらに置いておきます。
内容としては、前半はQt/3Dで、後半はBulletについて話しました。
前半については自分もQt/3Dは初めてだったのでなるべく分かりやすい資料を作っていたのですが、後半のBulletは関西ゲームプログラミング勉強会のものをほとんどそのまま使っていたら、自分で話していて、あぁコード量多くて、普段やっていない人には分かりづらいな。と感じてしまい、その点では反省しているところです。
ただ、一応デモとして見えるものは作っていたので、なんとなくでも理解していただけたようなので、それは良かったです。
あと、最初にQtで作っていたものとして、3Dモデラをちょっと紹介していましたが、言葉足らずでメタセコイアの開発者と間違えられたようで、どうもすみません。
ちなみに3Dモデラの方は現在再開発中で、ひと通りできたらシェアウェアとして公開しようかと考えております。

他の方の発表では、Qt5の紹介、QMLのライブコーディング、MeeGoの紹介と名前は知っているけど、実際はあまり理解していなかったものを詳しく聴けたため、とても面白かったです。
特にQMLは手軽にアニメーションできるUIを作れるので楽で良いなと思いました。

懇親会は、名古屋に結構長く居るのに行ったことがなかった世界の山ちゃんでした。
他の勉強会とはまた違った方向性の面白さがあり、懇親会中ずっと話しておりました。
それと、とにかく言われていたことは「英語を勉強しろ」でした。
英語を勉強して、海外に行きましょうかな。本気で。

二次会として立ち飲み居酒屋に行って、また色々話させて頂き、最後まで楽しい時間を過ごせました。
次回があれば、またなんだか発表しそうな雰囲気が漂っていましたが、時間がある限り、いくらでも発表しますので是非お願いいたします。

最後にですが、本当に暑い日でした。
名古屋駅前は40度近くの外気温があり、汗が止めどなく出る状態でした。

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

Qt/3Dのインストール

今週末の土曜にQt名古屋勉強会でQt/3Dについて話すこともあり、インストール方法くらいは書いておこうかと思います。
情報としては、公式ドキュメントとほぼ同じです。
対象とする環境は、Windows・VisualStudioとなります。

Qt/3Dは、以前からあったOpenGLまわりをより充実させたもので、最近のOpenGLに合わせた設計となっています。
具体的にはGLSLシェーダを用いたレンダリングですが、これだけだと些か面倒くさいところもあるので、もう一段階ラッパしており、従来の固定機能みたいな使いかたもできます。
単純な図形や、カメラ・ライトなどをシーングラフとして持つこともできるため、非常に扱いやすくなっています。

将来的(Qt5以降?)では、標準のものとして統合されるでしょうが、現段階(Qt4.7)ではQtLabの方にあるので、自分でインストールする必要があります。
今回は、このインストール方法を簡単に紹介します。

ソースコードの入手

ソースコードはQtLabのgitリポジトリに存在していますため、WindowsならばTortoiseGitなどの環境を構築しておいてください。
あらかじめインストールしていたQtのディレクトリ(例えば、C:\Qt4.7)以下にディレクトリ(ここではqt-labs)を作成し、そこにqt3dのソースコードをcloneします。
gitリポジトリのURLは「git://gitorious.org/qt-labs/qt3d.git」です。

インストール

ソースコードのビルドは、Qt本体と同じように行います。
VisualStudioのプロンプトを開き、ソースコードを取得したディレクトリ(C:\Qt4.7\qt-labs\qt3d)に移動します。
そして、以下のコマンドによりビルド・インストールが行えます。
なお、qmake.exeへのパスは事前に通しておいてください。

qmake.exe
nmake
nmake install

これによって、元のQtのディレクトリ以下にQt/3D関連のヘッダファイルやライブラリがインストールされます。

.proファイルの設定

Qt/3D関連の機能を使う際には、対応したライブラリをリンクする必要があります。
公式ドキュメントでは「CONFIG+=system_3ds」とすれば良さそうですが、これだと上手くいかなかったため、以下のように直接ライブラリをリンクするように指定しました。

LIBS += -lQt3D

実際に3Dオブジェクトをレンダリングするためには、QGLViewなどを使っていけば良いのですが、今回は割愛します。
サンプル・チュートリアルを一通り見れば、大体は分かるかと思います。
Qt名古屋勉強会では、ここらへんの話を発表する予定です。

テクスチャ表示

今回はテクスチャを張ります。
今回もコードは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();
}

まとめ

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

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