2012年

6月

23日

OpenTKを使ってみた【音声処理・入力処理編】

OpenTK(OpenToolKit)とは?

 

C#でOpenGLを扱えるようにしてくれるラッパークラスライブラリで、ここで私が何度か紹介した「Tao framework」と同じようなものです。 

 

一通り使ってみて、それぞれの大まかな機能の違いをまとめてみました。もしかすると間違えている箇所があるかもしれません。

Tao frameworkとOpenTKの機能の差 (○:対応 ×:未対応 △:問題有)

Glu Glut OpenGL ES SDL DevIL OpenAL OpenCL Cgfx 数学補助
Tao
× × ×
OpenTK △※1 × × × △※2 ○※3 ×

※1. OpenTKのGluについて

OpenTK.Graphics.Gluとして用意はされていますが、クラスにObsolete要素(使用非推奨)が付いてます。

 

説明を見る限りでは、OpenTKのMath関数で代用してくださいとのことですが、代用できない処理がいくつかある気がする。

 

 

※2. OpenTKのOpenALについて

Al、Alcは問題なく使えます。Alutは「OpenTK.Audio.Alut」として用意されていますが、クラスにObsolete要素が付いているので使用するのは避けたほうがいいでしょう。

 

ちなみにAlは「OpenTK.Audio.Al」と「OpenTK.Audio.OpenAl.Al」

Alcは「OpenTK.Audio.Alc」と「OpenTK.Audio.OpenAl.Alc」

何故かそれぞれ二つ用意されており、どちらを使っても問題なく動きます。

 

 

※3. OpenTKのOpenCLについて

まだテスト段階のようです。

名前空間を探してみたものの、どこに定義されているのかが分からない・・・

数学補助とは、Vector2~4クラスやMatrixクラス、Quaternionクラスのことです。DirectXやXNAを使ったことのある方なら馴染みがあると思います。

 

OpenTKは数学系に強く、Tao frameworkは画像系に強いですね。まぁOpenTKに画像ファイルの読み込みは備わってませんが、C#には「System.Drawing.Bitmap」クラスがあるので有名どころのフォーマットで言えば「.bmp」、「.jpg」、「.png」を読み込むことは可能です。

 

Tao.DevILを使えば「.dds」や「.tga」の読み込みも可能となりますが、特に必要としないならOpenTKで充分でしょう。

 

C#でOpenGLを扱うには「OpenTK」や「Tao framework」以外にも、「GLSharp」「C# wrapper for OpenGL」というものもあります。色々使ってみて扱いやすいものを探してみるのも面白いかもしれません。

 

ちなみに「OpenTK」はマルチプラットフォームですがWGL(Windows用OpenGL)には対応しておらず、「Tao framework」、「GLSharp」、「C# wrapper for OpenGL」はそれぞれWindows専用らしいです。

 

GLSharp 日本の方が開発してるようです

C# wrapper for OpenGL(SharpGL)

ここからが本編

そもそも何故こんなことをしてるかというと、3年ぶりにゲームを作りたくなりましてね。DirectXやXNAでのゲーム開発の経験はあるんですけどOpenGLでのゲーム開発はやったことが無いので、勉強がてらOpenTKを選択しました。

 

C#のBulletエンジン「Bullet Sharp」も対応してるので物理演算も搭載してみようと考えています。

 

さて、ようやく今回のタイトルにある「音声処理」「入力処理」について触れていきます。

 

 

「音声処理」はもちろん効果音やBGMの再生処理です。

DirectXだと「DirectSound」、「DirectMusic」、場合によっては「DirectShow」を使う人もいるかもしれません。が、DirectShowは動画処理に使うイメージが強いですし、DirectMusicはDirectSoundのバッファを通して音声を再生してるので、結局のところDirectSoundを使ってる人が多いのではないでしょうか?

 

 

「入力処理」はキーボードやマウス、ゲームコントローラーのボタン入力処理です。

DirectXだと「DirectInput」や「XInput」が一般的です。XInputはXBox360コントローラしか受け付けませんが、DirectInputに比べて実装が遥かに簡単なので使ってる人も多いと思います。DirextInputだとコントローラーの振動も出来ませんからね。

 

 

今回はC# + OpenGLで開発するのでどちらも扱えません(使えないことも無いけど、それなら端からDirectXで作る)。音声は「OpenTK.Audio(OpenAL)」、入力は「OpenTK.Input」を使うことにしました。

 

色々試行錯誤して実装は出来ましたが、色々と詰まったところがあるので今回まとめておきます。いつものごとく、日本語サイトの情報が少ないもので・・・

 

ちなみに今回使用したOpenTKは「2012-03-15」版です。

ダウンロードはOpenTKの公式サイトから。

「Download OpenTK」ではなく「Nightly builds」を押して、そこから目的のバージョンのものを探してダウンロードしてください。「Download OpenTK」を選択すると自動的に「2010-10-06」版のダウンロードが始まります。

OpenTKでのOpenAL使用方法(Alut抜き)

OpenALの使い方を調べてもAlutで初期化&読み込みをしてることが多く、情報が少なくて困りましたがなんとか出来ました。Alutを使う場合、初期化は1行で済みます。

 

OpenTK.Audio.Alut.Init(); 又は

OpenTK.Audio.Alut.Init(IntPtr argc, IntPtr argv);

 

しかし今回はAlutを使わずに、初期化を自分でやってしまいます。

終了処理も含めこのようになりました。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
using System;
using System.Windows.Forms;
using OpenTK;
using OpenTK.Audio;

namespace OpenALSample
{
    public class Sound
    {
        #region 動的メンバ
        /// <summary>
        /// ソース、バッファの初期値
        /// </summary>
        static readonly int DEFAULT_SOURCE_VALUE = 0;

        /// <summary>
        /// デバイス
        /// </summary>
        static IntPtr Device { get; set; }

        /// <summary>
        /// コンテキスト
        /// </summary>
        static ContextHandle CurrentContext { get; set; }

        /// <summary>
        /// レンダラー
        /// </summary>
        public static string Renderer { get; private set; }

        /// <summary>
        /// ベンダー
        /// </summary>
        public static string Vendor { get; private set; }

        /// <summary>
        /// バージョン
        /// </summary>
        public static string Version { get; private set; }
        #endregion
        
        /// <summary>
        /// コンストラクタ(インスタンス化禁止)
        /// </summary>
        Sound() { }

        /// <summary>
        /// 初期化
        /// </summary>
        public static bool Initialize()
        {
            try
            {
                // デバイスを開く
                Device = Alc.OpenDevice(null);
                if (Device == null)
                    throw new Exception("OpenALデバイスの作成に失敗しました.");

                // コンテキスト作成
                CurrentContext = Alc.CreateContext(Device, (int[])null);
                if (CurrentContext == null)
                    throw new Exception("OpenALコンテキストの作成に失敗しました.");

                // 作成したコンテキストをカレント設定
                Alc.MakeContextCurrent(CurrentContext);

                // OpenALのバージョン取得
                Version = AL.Get(ALGetString.Version);
                Vendor = AL.Get(ALGetString.Vendor);
                Renderer = AL.Get(ALGetString.Renderer);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                return false;
            }

            return true;
        }

        /// <summary>
        /// 開放
        /// </summary>
        public static void Release()
        {
            // コンテキスト開放
            if (CurrentContext != ContextHandle.Zero)
            {
                Alc.MakeContextCurrent(ContextHandle.Zero);
                Alc.DestroyContext(CurrentContext);
            }

            // デバイスを閉じる
            if (Device != IntPtr.Zero)
                Alc.CloseDevice(Device);

            // 初期化
            CurrentContext = ContextHandle.Zero;
            Device = IntPtr.Zero;
        }
    }
}

次に音声ファイルの読み込み。Alutだとこうなります。


Al.GenSources( 1, out source );
buffer = Alut.CreateBufferFromFile( "se.wav" );
Al.Source( source, ALSourcei.Buffer, buffer );

 

mp3が読み込めるかは不明だが、wavさえ読み込めればなんとでもなる。ついでに一時停止、停止、ループ再生、音量調整の方法も書いておきます。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
public class Sound
{
    /// <summary>
    /// 最小音量
    /// </summary>
    static readonly float MINIMUM_VOLUME = 0f;

    /// <summary>
    /// 最大音量
    /// </summary>
    static readonly float MAXIMUM_VOLUME = 1f;

    /// <summary>
    /// ソース
    /// </summary>
    public int Source { get; private set; }

    /// <summary>
    /// ソース開放済み
    /// </summary>
    public bool IsDisposed { get; private set; }

    /// <summary>
    /// ループ設定
    /// </summary>
    public bool IsLooped { get; set; }

    /// <summary>
    /// 音量設定
    /// </summary>
    public float Volume
    {
        set
        {
            // 0~1の範囲に設定(MathHelper.Clampは自作)
            _volume = MathHelper.Clamp(value, MINIMUM_VOLUME, MAXIMUM_VOLUME);
            AL.Source(Source, ALSourcef.Gain, _volume);
        }
        get
        {
            return _volume;
        }
    }

    float _volume;

    /// <summary>
    /// コンストラクタ(インスタンス化禁止)
    /// </summary>
    Sound()
    {
        Source = DEFAULT_SOURCE_VALUE;
        IsDisposed = false;
        IsLooped = false;
        _volume = 1f;
    }

    /// <summary>
    /// サウンド読み込み
    /// </summary>
    /// <param name="filename"></param>
    /// <returns></returns>
    public static Sound LoadFile(string filename)
    {
        // 正常に初期化をしてなければ終了
        if (Device == null || CurrentContext == null)
            return null;

        Sound sound = null;
        AudioReader read = null;
        int bufferID = DEFAULT_SOURCE_VALUE, sourceID = DEFAULT_SOURCE_VALUE;

        try
        {
            // ファイルの存在をチェック
            if (!File.Exists(filename))
                throw new Exception("サウンドファイルが存在しません.");

            // バッファの確保
            AL.GenBuffers(1, out bufferID);
            if (!AL.IsBuffer(bufferID) || AL.GetError() != ALError.NoError)
                throw new Exception("バッファ取得に失敗しました.");

            // 音源ファイル読み込み(ここからがAlutとの差異点)
            read = new AudioReader(filename);
            AL.BufferData(bufferID, read.ReadToEnd());
            
            if (AL.GetError() != ALError.NoError)
                throw new Exception("サウンド読み込みに失敗しました.");

            // ソース確保
            AL.GenSources(1, out sourceID);
            if (!AL.IsSource(sourceID) || AL.GetError() != ALError.NoError)
                throw new Exception("ソース取得に失敗しました.");
            
            // ソースとバッファを関連付ける
            AL.Source(sourceID, ALSourcei.Buffer, bufferID);

            // サウンドクラス作成
            sound = new Sound { Source = sourceID };
        }
        catch (Exception ex)
        {
            // ソース開放
            if (AL.IsSource(sourceID))
            {
                AL.DeleteSource(sourceID);
                sourceID = DEFAULT_SOURCE_VALUE;
            }

            sound = null;
            MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
        }
        finally
        {
            // リーダーを開放
            if (read != null)
            {
                read.Dispose();
                read = null;
            }

            // バッファ開放
            if (AL.IsBuffer(bufferID))
            {
                AL.DeleteBuffer(bufferID);
                bufferID = DEFAULT_SOURCE_VALUE;
            }
        }

        return sound;
    }

    /// <summary>
    /// 開放
    /// </summary>
    public void Dispose()
    {
        if (!IsDisposed)
        {
            // ソース開放
            if (Source != DEFAULT_SOURCE_VALUE)
            {
                // 音声を停止して開放する
                AL.SourceStop(Source);
                AL.DeleteSource(Source);
            }

            // 初期化
            Source = DEFAULT_SOURCE_VALUE;
            IsDisposed = true;
        }
    }

    /// <summary>
    /// 再生
    /// </summary>
    public void Play()
    {
        ALSourceState state = AL.GetSourceState(Source);

        // 再生中で無ければ再生開始
        if (state != ALSourceState.Playing)
        {
            AL.Source(Source, ALSourceb.Looping, IsLooped);
            AL.SourcePlay(Source);
        }
    }

    /// <summary>
    /// 停止
    /// </summary>
    public void Stop()
    {
        AL.SourceStop(Source);
    }

    /// <summary>
    /// 停止
    /// </summary>
    public void Pause()
    {
        AL.SourcePause(Source);
    }
}

OpenTK.Audio.AudioReaderクラスで音源ファイルを末端まで読み込み、そのデータを確保したバッファに書き込みます。それ以外はAlutのやり方と同じです。

OpenTKでのジョイスティックの入力情報取得

むしろ今回本当に困ったのはコッチ。本当に情報が少なくて、日本語のものはまず皆無でした。結局自分で解決したし。

 

OpenTK.Input.InputDriverというクラスに「Keybord」、「Mouse」、「Joysticks」というプロパティが定義されていて、これらから入力情報を取得することが出来る。

 

ただしInputDriverを使うにはOpenTK.GameWindowクラスが必要なので、アプリケーションを作る場合は「GLControl」ではなく「GameWindow」ありきになってしまう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;
using System.Windows.Forms;

namespace OpenTKSample
{
    static class Program
    {
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            // スタイル設定
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            // ウィンドウ作成
            MainForm form = MainForm.CreateWindow(
                "OpenTK + DevILでゲーム開発", // タイトル
                640, 480, // 画面サイズ
                32, // バッファのカラービット数
                24, // 深度バッファのビット数
                8,  // ステンシルバッファのビット数
                8); // マルチサンプリング数

            if (form != null)
            {
                // 実行(1秒間に60フレーム更新)
                form.Run(60);

                // 終了
                form.Close();
                form = null;
            }
        }
    }
}

次はMainFormクラスの中身。

GameWindowを継承して、初期化、更新、描画、開放処理にそれぞれイベントを追加しています。今回大事なところは90行目です。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
using System;
using System.Drawing;
using OpenTK;
using OpenTK.Graphics.OpenGL;

namespace OpenTKSample
{
    public sealed class MainForm : GameWindow
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainForm(string title,
                       int width, int height,
                       int bits,
                       int depth,
                       int stencil,
                       int samples)
                       
            : base(width, height,
                   new OpenTK.Graphics.GraphicsMode(
                      new OpenTK.Graphics.ColorFormat(bits),
                      depth,
                      stencil,
                      samples),
                      
                   title,
                   GameWindowFlags.Default,
                   DisplayDevice.Default)
        {
            // リサイズ不可
            this.WindowBorder = OpenTK.WindowBorder.Fixed;
            
            // イベント設定
            this.UpdateFrame += UpdateEvent;
            this.RenderFrame += RenderEvent;
            this.Load += LoadEvent;
            this.Unload += UnloadEvent;
        }

        /// <summary>
        /// ウィンドウ作成
        /// </summary>
        /// <param name="title"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="bits"></param>
        /// <param name="depth"></param>
        /// <param name="stencil"></param>
        /// <param name="samples"></param>
        /// <returns></returns>
        public static MainForm CreateWindow(string title, 
                                           int width, int height,
                                           int bits,
                                           int depth,
                                           int stencil,
                                           int samples)
        {
            return new MainForm(title,
                                width, height,
                                bits,
                                depth,
                                stencil,
                                samples);
        }
        
        /// <summary>
        /// 読み込みイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
       void LoadEvent(object sender, EventArgs e)
        {
            GL.ClearColor(Color.CornflowerBlue);
        }

        /// <summary>
        /// 終了イベント
        /// </summary>
        /// <param name="e"></param>
        void UnloadEvent(object sender, EventArgs e) { }

        /// <summary>
        /// 更新イベント
        /// </summary>
        /// <param name="e"></param>
        void UpdateEvent(object sender, FrameEventArgs e)
        {
            // 入力情報の更新※
            InputDriver.Poll();
        }

        /// <summary>
        /// 描画イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void RenderEvent(object sender, FrameEventArgs e)
        {
            // 画面クリア
            this.MakeCurrent();
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            // 画面の更新
            this.SwapBuffers();
        }
    }
}

GameWindowクラスには以下の4つの入力処理用プロパティが宣言されています。

 

OpenTK.Input.KeybordDevice型 Keybord

OpenTK.Input.MouseDevice型 Mouse

OpenTK.Input.JoystickDevice[]型 Joysticks

OpenTK.Input.InputDriver型 InputDriver

 

このうち上記のの三つはInputDriverからの値が入っており、InputDriver型にはObsolete要素(使用非推奨)が付いてます。

 

「Keyboard、Mouse、Joysticksからそれぞれ値を取得できるなら特に問題はないのでは?」と思うでしょうが、実は90行目のJoyDriver.Poll()は入力状態を更新する処理で、これを呼び出さないと入力情報は更新されないようになっています。

 

特にJousticksの値を取得するにはPollメソッドを必ず呼ぶ必要があります。何故かKeyboardとMouseだけは呼ばなくても自動で値が更新されるようになってまして、この謎仕様のせいで大分悩まされました。

 

それぞれのDevice型には入力更新の処理が無いのでInputDriver.Poll()の呼び出しが必須なのですが、肝心のInputDriverにはObsolete要素がつけられてる始末・・・

 

「使わないでくれ」となってますが、コレ使わないとJoysticksの正しい値が取得出来ないし、どうしろっていうんだ・・・・・・

どうも色々と調べてみたところ、OpenTKは幾つかのバグがありまだ開発途中段階のライブラリらしいです。なんでも数学関連の処理にも一部問題があるとか。

 

でもMatrixやQuaternionを扱えるというだけでもこのライブラリを選ぶ価値はあると思います。今までOpenGLで行列演算をするときはわざわざSlimDXをインポートしてたので、その必要もなくなります。

 

OpenGLの関数パラメータも全て列挙体で定義されていてとても分かりやすくなってます。

 

例)

GL_TEXTURE_2DTextureTarget.Texture2D

GL_CULLFACEEnableCap.CullFace

 

初めてOpenGLに触ったとき、関数の引数でどの「GL_~」が有効な値なのか分からなかった方々って結構いらっしゃるのではないのでしょうか?

 

「GL_UINT, GL_INT型じゃなくて、列挙方型で定義してくれよ」って思った人が少なからずいると思います。その点OpenTKはその辺りはとても分かりやすく出来てて、中々初心者向きだと思います。

 

C#でOpenGLをやるのであれば、Tao frameworkよりはOpenTKをオススメします。

幾つかの不具合や変な仕様が多々ありますが、その辺りはOpenTKのこれからの更新に期待したいところですね。色々と楽しみなライブラリです。

追記

JoystickDeviceのボタン割り当てをXBox360コントローラーとPS2コントローラーで調べてみました。ボタンは最大16個、軸は最大10本まで対応してるようです。

 

 

PS2アナログコントローラー(SCPH-10010) 軸は6本 ボタンは12個
JoystickAxis.Axis0 => 左スティックX軸

 ※アナログスティックが有効でない場合、十字キーX軸


JoystickAxis.Axis1 => 左スティックY軸

 ※アナログスティックが有効でない場合、十字キーY軸


JoystickAxis.Axis2 => 右スティックX軸
JoystickAxis.Axis3 => 右スティックY軸
JoystickAxis.Axis4 => 十字キーX軸
JoystickAxis.Axis5 => 十字キーY軸

 

JoystickButton.Button0 =>
JoystickButton.Button1 =>
JoystickButton.Button2 => ×
JoystickButton.Button3 =>
JoystickButton.Button4 => L2
JoystickButton.Button5 => R2
JoystickButton.Button6 => L1
JoystickButton.Button7 => R1
JoystickButton.Button8 => SELECT
JoystickButton.Button9 => START
JoystickButton.Button10 => 左スティック押し込み
JoystickButton.Button11 => 右スティック押し込み

 

 

XBox360コントローラー 軸は7本 ボタンは10個

JoystickAxis.Axis0 => 左スティックX軸

JoystickAxis.Axis1 => 左スティックY軸

JoystickAxis.Axis2 => LT, RT(-1~0 => LT/0~1 => RT)

JoystickAxis.Axis3 => 右スティックX軸
JoystickAxis.Axis4 => 右スティックY軸
JoystickAxis.Axis5 => 十字キーX軸
JoystickAxis.Axis6 => 十字キーY軸


JoystickButton.Button0 => A
JoystickButton.Button1 => B
JoystickButton.Button2 => X
JoystickButton.Button3 => Y
JoystickButton.Button4 => LB
JoystickButton.Button5 => RB
JoystickButton.Button6 => BACK
JoystickButton.Button7 => START
JoystickButton.Button8 => 左スティック押し込み
JoystickButton.Button9 => 右スティック押し込み