2012年

3月

01日

OpenGL+DevILで画像を入出力する

実はMMDfromColladaのお話。

DirectX版でテクスチャが反映されない問題について、OpenGL版も改めて調べてみると色々と問題が起きた。

 

前回までのバージョンでは画像読み込みにSdlライブラリのSDL_imageAPIを使用していた。問題なく動いてるように見えたが、以下のように目が反映されず白目をむく不具合を発見した。

これは内部でテクスチャサイズを実際の値より一回り大きい2の階上サイズに拡大してやることで解決した。このテクスチャの場合「666×612」→「1024×1024」になる。

 

しかしOpenGL2.0以降だと2の階上じゃないサイズでも読み込めるような仕組みになっているらしい。では何故失敗したのだろうか?よくわからない。

もう一つの問題点。コチラのほうが影響が大きい。

 

OpenGL版は1、4、8、16bit画像には対応してなかったので実装を試みた。

 

画像はピクセルデータの配列を所持している。32、24、16bit画像だとこのピクセル毎に色データが保存されている。1、4、8bit画像の場合は色 データの変わりにパレットのインデックスが格納されていて、その番号のパレットへアクセスすることで色データを取得できる。

 

で、その肝心のパレットデータが取得できなかったのだ。

以下はSDLの画像読み込みに必須となる構造体の内容↓

  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
// Texture.cs

// SDL_Surface構造体
public struct SDL_Surface
{
    // 概要:
    //     surface clip rectangle
    public Sdl.SDL_Rect clip_rect;
    //
    // 概要:
    //     Surface flags
    public int flags;
    //
    // 概要:
    //     Pixel format Pointer to SDL_PixelFormat
    public IntPtr format;
    //
    // 概要:
    //     format version, bumped at every change to invalidate blit maps
    public int format_version;
    //
    // 概要:
    //     Height of the surface
    public int h;
    //
    // 概要:
    //     Hardware-specific surface info
    public IntPtr hwdata;
    //
    // 概要:
    //     Allow recursive locks
    public int locked;
    //
    // 概要:
    //     info for fast blit mapping to other surfaces
    public IntPtr map;
    //
    public int offset;
    //
    // 概要:
    //     Length of a surface scanline in bytes
    public short pitch;
    //
    // 概要:
    //     Pointer to the actual pixel data Void pointer.
    public IntPtr pixels;
    //
    // 概要:
    //     Reference count -- used when freeing surface
    public int refcount;
    //
    public int unused1;
    //
    // 概要:
    //     Width of the surface
    public int w;
}

// SDL_PixelFormat構造体
public struct SDL_PixelFormat
{
    // 概要:
    //     Precision loss of each color component (2[RGBA]loss)
    public byte Aloss;
    //
    // 概要:
    //     Overall surface alpha value
    public byte alpha;
    //
    // 概要:
    //     Binary mask used to retrieve individual color values
    public int Amask;
    //
    // 概要:
    //     Binary left shift of each color component in the pixel value
    public byte Ashift;
    //
    // 概要:
    //     The number of bits used to represent each pixel in a surface. Usually 8,
    //     16, 24 or 32.
    public byte BitsPerPixel;
    //
    // 概要:
    //     Precision loss of each color component (2[RGBA]loss)
    public byte Bloss;
    //
    // 概要:
    //     Binary mask used to retrieve individual color values
    public int Bmask;
    //
    // 概要:
    //     Binary left shift of each color component in the pixel value
    public byte Bshift;
    //
    // 概要:
    //     The number of bytes used to represent each pixel in a surface. Usually one
    //     to four.
    public byte BytesPerPixel;
    //
    // 概要:
    //     Pixel value of transparent pixels
    public int colorkey;
    //
    // 概要:
    //     Precision loss of each color component (2[RGBA]loss)
    public byte Gloss;
    //
    // 概要:
    //     Binary mask used to retrieve individual color values
    public int Gmask;
    //
    // 概要:
    //     Binary left shift of each color component in the pixel value
    public byte Gshift;
    //
    // 概要:
    //     Pointer to the palette, or NULL if the BitsPerPixel>8 Pointer to Tao.Sdl.Sdl.SDL_Palette
    public IntPtr palette;
    //
    // 概要:
    //     Precision loss of each color component (2[RGBA]loss)
    public byte Rloss;
    //
    // 概要:
    //     Binary mask used to retrieve individual color values
    public int Rmask;
    //
    // 概要:
    //     Binary left shift of each color component in the pixel value
    public byte Rshift;
}

// SDL_Palette構造体
public struct SDL_Palette
{
    // 概要:
    //     Array of Tao.Sdl.Sdl.SDL_Color structures that make up the palette.
    public Sdl.SDL_Color[] colors;
    //
    // 概要:
    //     Number of colors used in this palette
    public int ncolors;
}

IntPtrの部分はそれぞれ構造体のポイントで、

SDL_Surface.format → SDL_PixelFormat*型

SDL_Surface.pixels → byte配列

SDL_PixelFormat.palette → SDL_Palette*型

となっている。

 

値も以下のように↓

1
2
3
4
5
6
unsafe
{
    byte* pixels = (byte*)surface.pixel;
    Sdl.SDL_PixelFormat* format = (Sdl.SDL_PixelFormat*)surface.format;
    Sdl.SDL_Palette* palette = (Sdl.SDL_Palette*)surface.palette;
}

と、型キャストするだけで簡単に取得できる.

ところが実際はコンパイル前にこんなエラーが出た。

 

「エラー    1    マネージ型のアドレスの取得、マネージ型のサイズの取得、またはマネージ型へのポインターの宣言が実行できません 」

 

原因はSDL_Paletteポインタへの型キャストによるもの。

後で調べてみて分かったのだが、メンバ変数に配列を使っているのがどうも駄目らしい。C#の配列はマネージ型なので、メンバに使用したことでポインタにキャストすることが出来ない。

 

SDL_Color自体は4つのbyte変数を持ってるだけなので「byteやintポインタに変換すれば読み取れるのでは?」と試してみたが駄目だった。

 

結局SDLを諦めてDevILに乗り換えた。

 

追記:

最近知ったのだが、C#だとSystem.Runtime.Interopservices.Marshal.PtrToStructureメソッドを使えばIntPtr型を構造体に変換できるらしい。結局は私の無知が原因でした。まぁDevILの方が色々と便利なので今後もDevILを使っていくつもりだけどね。

そもそも私がSDLやDevILを使おうと思ったのは、C#の既存API「System.Drawing.Bitmap」が.tgaファイルをサポートしてないからである。Bitmapクラスにtga読み込み機能があればこんなことをする必要も無かったのだが、致し方ない。

 

まぁ当初の私はTao FrameworkにSDLやDevILが搭載されていることは全く知らなかった。というかそのようなライブラリの存在すら知らなかった。

 

DevILは画像処理に特化したイメージライブラリ。OpenGLに似せて作られていて非常に馴染みやすい。旧名はOpenILというらしいが、OpenGLの開発元「シリコングラフィックス社」からの要求で現在の名前に改名したらしい。何故その名前にした?

 

SDLはゲームやマルチメディア関連といったソフトウェアを開発するためのライブラリで、画像処理を行うSDL_image、音声処理を行うSDL_mixer、フォントをサポートするSDL_ttf、図形描画をサポートするSDL_gfxが提供されている。

 

どちらもフリーのライブラリでOSに依存しないクロスプラットフォームである。

 

DevILの利点は多様なフォーマットのサポート画像出力が可能なことだろう。例えばDirectDrawのテクスチャ形式の.dds、Adobe PhotoShopの.psd等とにかく扱えるフォーマット数がSDLと比べて圧倒的に多い。

 

wikipediaによると、43のフォーマットの読み込み、17のフォーマットの書き込みに対応してるらしい。具体的なフォーマットが知りたい方はコチラを参考にするといいだろう。

DevIL Feature List

 

とりあえず公式のページと幾つか日本語のページを見つけたので参考にさせてもらった。相変わらず日本語の情報が少なくて困る。

DevIL 公式サイト

DevILを用いた画像ファイルの読み込み

OpenGLとDevILを使って1時間でお絵描きソフトを作る

 

公式によると、現在バージョン178までリリースされている。Taoに付属されてるバージョンは168なので若干古い。

 

まずはDevILの初期化からテクスチャ読み込みの流れ。

24、32bit画像だけを考慮した場合↓

 

 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
// DevIL初期化
Il.ilInit();
Ilu.iluInit();

// DevILバージョン取得
Console.WriteLine("\r\n>>>>>>DevIL:");
Console.WriteLine(string.Format(">>>>>>>>>Version={0}", Il.IL_VERSION));
Console.WriteLine(string.Format(">>>>>>>>>Vendor={0}", Il.IL_VENDOR));

// 画像読み込み
int bindID = 0;
IntPtr pixels = IntPtr.Zero;

try
{
    // テクスチャ名の生成
    bindID = Il.ilGenImage();
    if(bindID <= 0)
        throw new Exception("テクスチャ名生成に失敗しました.");

    Il.ilBindImage(bindID);

    // 画像読み込み
    bool result = Il.ilLoadImage(filename);
    if(!result)
        throw new Exception("テクスチャ読み込みに失敗しました.");

    // 画像サイズ取得
    int width = Il.ilGetInteger(Il.IL_IMAGE_WIDTH);
    int height = Il.ilGetInteger(Il.IL_IMAGE_HEIGHT);
    int depth = Il.ilGetInteger(Il.IL_IMAGE_DEPTH);
    
    // ピクセルの配列順序が左上→右下の場合、上下を反転させる
    if (Il.ilGetInteger(Il.IL_IMAGE_ORIGIN) == Il.IL_ORIGIN_UPPER_LEFT)
        Ilu.iluFlipImage();

    // ピクセルデータの格納場所を作成(強制的に32bit形式に変換する)
    pixels = Marshal.AllocCoTaskMem(width * height * 4);
    
    // ピクセルデータを取得
    Il.ilCopyPixels(0, 0, 0, // x, y, zのオフセット
                    width, height, depth, // x, y, z幅
                    Il.IL_RGBA, // ピクセルフォーマット
                    Il.IL_UNSIGNED_BYTE, // 1ピクセル辺りのデータ形式
                    pixels); // ピクセルデータを格納するポインタ

    if (Il.ilGetError() != Il.IL_NO_ERROR)
        throw new Exception("ピクセルデータの取得に失敗しました.");

    // ピクセルデータをコピーしたのでもはや不要
    Il.ilDeleteImage(bindID);
    bindID = 0;
}
catch (Exception ex)
{
    Console.WriteLine(string.Format(">>>エラー:{0}", ex.Message));
    
    // メモリを開放
    if(pixels != IntPtr.Zero)
    {
        try
        {
            Marshal.FreeCoTaskMem(pixels);
            pixels = IntPtr.Zero;
        } catch { }
    }
}
finally
{
    // イメージ開放
    if (bindID > 0)
    {
        Il.ilDeleteImage(bindID);
        bindID = 0;
    }
}

取得したピクセルデータをglTexImage2D又はgluBuild2DMipmapsに渡せばいい。ピクセルデータをコピーする必要が無ければilGetDataでピクセルデータをIntPtr型で取得できる。

 

ただこのコードだと問題が発生する。34~35行目のIlu.iluFlipImage、一定の画像幅(おそらく縦横のどちらかが2048以上)を超えたテクスチャでこれを実行すると奇妙な現象が起こる。

 

例えばMMDのてゐモデル。このモデルはテクスチャが全て「2048×2048」という巨大なサイズになっている。これを読み込むとこうなる。

 

ちなみにiluFlipImageをかけたらこうなる。

 

要するにどちらでも駄目なわけである。

この時のテクスチャがどのような向きになっているのか出力して確認してみた。

まずは通常のテクスチャ

これがデフォルトの向き

DevILで読み込んだ状態 iluFlipImageはかけてない

上下が反転している

iluFlipImageをかけた状態

めちゃくちゃになっている

このような事が起きる場合があるのでiluFlipImageはしないほうがよい。

 

まぁ大きなテクスチャを使用しなければそれでいいのだが、実際に大きなテクスチャを使うモデルが存在する限り対応しないわけにはいかないだろう。

 

ピクセルデータは拾えるので、自前で上下反転処理をやってしまう。

今回は32、24bitだけでなく他のbit数の画像も読み込めるようにしてみた。

もしかしたら他にいい方法があるかもしれない。

 

修正後↓

  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
// DevIL初期化
Il.ilInit();
Ilu.iluInit();

// DevILバージョン取得
Console.WriteLine("\r\n>>>>>>DevIL:");
Console.WriteLine(string.Format(">>>>>>>>>Version={0}", Il.IL_VERSION));
Console.WriteLine(string.Format(">>>>>>>>>Vendor={0}", Il.IL_VENDOR));

// 画像読み込み
int bindID = 0;
IntPtr pixels = IntPtr.Zero;

try
{
    // テクスチャ名の生成
    bindID = Il.ilGenImage();
    if(bindID <= 0)
        throw new Exception("テクスチャ名生成に失敗しました.");

    Il.ilBindImage(bindID);

    // 画像読み込み
    bool result = Il.ilLoadImage(filename);
    if(!result)
        throw new Exception("テクスチャ読み込みに失敗しました.");

    // 画像サイズ取得
    int width = Il.ilGetInteger(Il.IL_IMAGE_WIDTH);
    int height = Il.ilGetInteger(Il.IL_IMAGE_HEIGHT);
    int depth = Il.ilGetInteger(Il.IL_IMAGE_DEPTH);
    int bits = Il.ilGetInteger(Il.IL_IMAGE_BITS_PER_PIXEL);

    // ピクセルデータの格納場所を作成(強制的に32bit形式に変換する)
    pixels = Marshal.AllocCoTaskMem(width * height * 4);

    switch (bits)
    {
        case 32:
        case 24: // 16bit画像は読み込んだ時点で24bitカラーに修正される
            if (Il.ilGetInteger(Il.IL_IMAGE_ORIGIN) == Il.IL_ORIGIN_UPPER_LEFT)
            {
                // ピクセルデータを取得
               Il.ilCopyPixels(0, 0, 0, // x, y, zのオフセット
                    width, height, depth, // x, y, z幅
                    Il.IL_RGBA, // ピクセルフォーマット
                    Il.IL_UNSIGNED_BYTE, // 1ピクセル辺りのデータ形式
                    pixels); // ピクセルデータを格納するポインタ

                if (Il.ilGetError() != Il.IL_NO_ERROR)
                    throw new Exception("ピクセルデータの取得に失敗しました.");
            }
            else
            {
                unsafe
                {
                    // 直接値を書き換える
                    byte* srcPixels = (byte*)Il.ilGetData();
                    byte* dstPixels = (byte*)pixels;
                    int stride = Il.ilGetInteger(Il.IL_IMAGE_BYTES_PER_PIXEL);

                    for (int y = 0; y < height; y++)
                    {
                        for (int x = 0; x < width; x++)
                        {
                            // ピクセルにデータ格納
                            int dstIndex = (y * width * 4) + (x * 4);
                            int srcIndex = ((height - y - 1) * width * stride) + 
                                           (x * stride);

                            // RGB配列を逆向きにして格納する
                            dstPixels[dstIndex + 0] = srcPixels[srcIndex + 2];
                            dstPixels[dstIndex + 1] = srcPixels[srcIndex + 1];
                            dstPixels[dstIndex + 2] = srcPixels[srcIndex + 0];
                            dstPixels[dstIndex + 3] = (stride == 4 ? 
                                                      srcPixels[srcIndex + 3]:
                                                      byte.MaxValue);
                        }
                    }
                }
            }
            break;

        case 8: // 1bit、4bit画像は読み込んだ時点で8bitカラーに修正される
        
            /**
            何回かテストしたが、1、4、8bitカラーで
            上下反転を必要とするパターンが検出されなかった
            もしかするとコチラも反転処理が必要かもしれない
            **/
            
            unsafe
            {
                // 直接値を書き換える
                int* palette = (int*)Il.ilGetPalette();
                byte* srcPixels = (byte*)Il.ilGetData();
                byte* dstPixels = (byte*)pixels;
                int size = Il.ilGetInteger(Il.IL_IMAGE_SIZE_OF_DATA);
                
                for (int i = 0; i < size; i++)
                {
                    // パレットから色データを取得
                    Color color = Color.FromArgb(palette[srcPixels[i]]);

                    // ピクセルにデータ格納
                    int index = i * 4;
                    dstPixels[index] = color.R;
                    dstPixels[index + 1] = color.G;
                    dstPixels[index + 2] = color.B;
                    dstPixels[index + 3] = byte.MaxValue;
                }
            }
            break;
    }
    
    // ピクセルデータをコピーしたのでもはや不要
    Il.ilDeleteImage(bindID);
    bindID = 0;
}
catch (Exception ex)
{
    Console.WriteLine(string.Format(">>>エラー:{0}", ex.Message));
    
    // メモリを開放
    if(pixels != IntPtr.Zero)
    {
        try
        {
            Marshal.FreeCoTaskMem(pixels);
            pixels = IntPtr.Zero;
        } catch { }
    }
}
finally
{
    // イメージ開放
    if (bindID > 0)
    {
        Il.ilDeleteImage(bindID);
        bindID = 0;
    }
}

これで崩れることなく画像を読み込める。

DevILで画像出力も出来るようになったので組んでみた。

再び上下反転をしないとテクスチャが上下逆さの状態で出力されるので注意。

今回はRGB配列を入れ替える必要が無いので読み込みよりもシンプルになった↓

 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
IntPtr dstPixels = IntPtr.Zero;
byte[] buf = new byte[width * 4];
int size = buf.Length * height;
int bindID = 0;

try
{
    // テクスチャ名の生成
    bindID = Il.ilGenImage();
    if(bindID <= 0)
        throw new Exception("テクスチャ名生成に失敗しました.");
    
    // 領域確保
    dstPixels = Marshal.AllocCoTaskMem(size);

    unsafe
    {
        using (UnmanagedMemoryStream streamSrc = new UnmanagedMemoryStream(
            (byte*)pixels,
            size,
            size,
            FileAccess.Read))

        using (UnmanagedMemoryStream streamDst = new UnmanagedMemoryStream(
            (byte*)dstPixels,
            size,
            size,
            FileAccess.Write))
        {
            // 上下逆さにデータをコピーする
            for (int y = 0; y < height; y++)
            {
                streamSrc.Read(buf, 0, buf.Length);

                streamDst.Seek(
                    (height - y - 1) * buf.Length,
                    SeekOrigin.Begin);
                streamDst.Write(buf, 0, buf.Length);
            }
        }
    }

    Il.ilBindImage(bindID);
    
    // ピクセルデータを設定
    Il.ilTexImage(
        width, height, depth, // ピクセルデータの幅
        4, // チャンネル数
        Il.IL_RGBA, // ピクセルフォーマット
        Il.IL_UNSIGNED_BYTE, // 1ピクセル辺りのデータ形式
        dstPixels); // ソースのピクセルポインタ

    if (Il.ilGetError() != Il.IL_NO_ERROR)
        throw new Exception("ピクセルデータの設定に失敗しました.");

    // ファイル出力(ファイル形式は拡張子で自動判断する)
    Il.ilSaveImage(outputFilename);

    if (Il.ilGetError() != Il.IL_NO_ERROR)
        throw new Exception("ファイル保存に失敗しました.");
}
catch (Exception ex)
{
    Console.WriteLine(string.Format(">>>>>>エラー:{0}", ex.Message));
}
finally
{
    // メモリ開放
    if (dstPixels != IntPtr.Zero)
        Marshal.FreeCoTaskMem(dstPixels);

    // イメージ開放
    if (bindID > 0)
        Il.ilDeleteImage(bindID);
    
    dstPixels = IntPtr.Zero;
    bindID = 0;
    buf = null;
}

今回の修正でOpenGL版でテクスチャが反映されないことはなくなったと思います。

 

あとはDirectX版ですが、テクスチャがうまく反映できない場合以下の項目をチェックしてみてください。

 

・画像幅が大きすぎないか確認する

大きくてもなるべく2048までに抑えてください。

 

・画像幅が2の階上になっているかどうか確認する

2の会場になってないと読み込めない環境があるようです。

 

もうじきデバッグ機能をつけたバージョンをリリースします。緊急なのでたいした更新はありません。上記の方法でもテクスチャが反映されない方はデバッグの結果を私に報告していただけると助かります。

 

なるべく早くリリースできるように頑張ります。

今回はここまで。

 

8bit画像の読み込み方法に誤りがありました。コチラをご参照下さい。

続・OpenGL+DevILで画像読み込み