2012年

2月

23日

Colladaを読み込んで描画したかっただけ

昨日の今日でColladaViewerを自作してみたくなった。

 

AndroidでDaeモデル読み込んでスキンメッシュアニメーションやったことあるけど、あの時はライブラリとか使わずに自力でデータを拾ってたので、今回はColladaのライブラリを使ってやってみることにした。

 

フォルダの隅っこに長い間圧縮されっぱなしで放置してたCollada DOMを解凍するときがようやくきた・・・

 

今回使用したのはCollada DOM 2.2。VC++2008でやってみた。

 

久しぶりのVC++に戸惑いつつ開発環境を整えた。昔のコードを探し出してWindowsアプリケーションを作成するところからはじめる。C++言語は3年間ずっと使ってたはずなのに1年程のブランクがあっただけでこのざまである。

思うようにコードが打てない。C#が便利すぎるのがいけないんだ。

 

そして苦戦しながらもまずはOpenGL(WGL)でポリゴンが描画できるところまで組んだ。

こんな感じ。画像では分からないが、赤いポリゴンがY軸に沿って回転し続けている。

左上はFPS。おかげでOpenGLの機能で文字列を描画する方法を覚えることが出来た。

で、早速肝心のCollada DOMの導入を始めた。以下のサイトを参考にさせていただきました。

COLLADA DOMを使った3Dモデルデータの読み込み

collada調べ始めた

 

まずプロジェクトの設定から。

「プロジェクトのプロパティ」から「構成プロパティ-全般」の「文字セット」を「Unicode文字セットを使用する」に設定する。これはCollada DOMライブラリのVCプロジェクトがUnicodeでコーディングされてるかららしい。

 

「C/C++-コード生成」の「ランタイムライブラリ」を「マルチスレッド  デバッグ DLL」、Releaseは「マルチスレッド DLL」に設定する。これはCollada DOMが「MSVCRxx.dll」(xxにはVCのバージョン数値)というC++のランタイムライブラリに依存しているので必要となるとか。VC2008なら 「MSVCR90.dll」、VC2010なら「MSVCR100.dll」と変わるので注意。

 

次にインクルルードディレクトリとライブラリディレクトリ。

collada-dom\dom\include

collada-dom\dom\include\1.4

collada-dom\dom\external-libs\boost

 

collada-dom\dom\build\vc9-1.4-d

collada-dom\dom\external-libs\libxml2\win32\lib

collada-dom\dom\external-libs\boost\lib\vc9

collada-dom\dom\external-libs\pcre\lib\vc9

collada-dom\dom\external-libs\minizip\win32\lib

 

これらを追加する。追加する前に先に"collada-dom\dom\projects\vc9-1.4\dom.sln"をコンパイルしておく必要がある。コンパイルするソリューションは使ってるVCのバージョンで異なる。

 

「C/C++-プリプロセッサ」に"BOOST_ALL_NO_LIB"を追加。

 

試しにコンパイルが通るかどうかコードを書いてみた。↓

 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
// Main.cpp
#include "WglWindow.h"

// Collada
#include <dae.h>

#pragma comment(lib, "libcollada14dom21-sd.lib")
#pragma comment(lib, "libxml2_a.lib")
#pragma comment(lib, "zlib.lib")
#pragma comment(lib, "wsock32.lib")
#pragma comment(lib, "libboost_filesystem-d.lib")
#pragma comment(lib, "libboost_system-d.lib")
#pragma comment(lib, "pcre-d.lib")
#pragma comment(lib, "pcrecpp-d.lib")
#pragma comment(lib, "minizip-d.lib")

/**
 * メイン
**/
int main()
{        
    // メモリリーク探索用
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

    // Daeオブジェクト作成
    DAE *dae = new DAE();

    // OpenGLウィンドウ作成
    WglWindow* wgl =
        WglWindow::SetOpenGLWindow((LPCWSTR)"WGLでOpenGL", 800, 600);

    if(NULL != wgl)
    {
         // ループ
        while(wgl->Loop())
        {
        }

        // 開放
        WglWindow::Release();
        delete dae;

        return 0;
    }

    return -1;
}
  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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
// WglWindow.cpp
#include "WglWindow.h"
#include "fps.h"

#pragma comment (lib, "opengl32.lib")
#pragma comment (lib, "glu32.lib")

//-------------------------------------------
//     メッセージ処理のコールバック

//             引数
//                     hWnd    ウィンドウハンドル
//                     msg             メッセージ
//                     wParam  UINT型のパラメータ
//                     lParam  LONG型のパラメータ

//             戻り値     処理結果を格納したLRESULT型
//-------------------------------------------
static LRESULT CALLBACK WndProc(HWND hWnd,
                                UINT msg,
                                WPARAM wParam,
                                LPARAM lParam) {
    switch(msg)
    {
        case WM_CREATE:
            // ループタイマー設定
            SetTimer(hWnd, TIME_ID, LOOP_TIME, NULL);
            break;

        case WM_TIMER:
            // 描画イベント発生
            if(wParam == TIME_ID)
                InvalidateRgn(hWnd, NULL, false);
            break;

        case WM_PAINT:
            if(WglWindow::singleton != NULL)
                WglWindow::singleton->Invalidate();
            break;

        case WM_DESTROY: // ウィンドウ破棄メッセージ
            PostQuitMessage(0); // WM_QUITを送りループを終了
            break;

        case WM_SYSKEYDOWN: // システムキーを使用不能にする
            return 0;

        case WM_KEYDOWN: // 終了宣言
            if(VK_ESCAPE == wParam)
                SendMessage(hWnd, WM_CLOSE, NULL, NULL);
            break;

        case WM_CLOSE: // クローズボタン
            DestroyWindow(hWnd);
            break;

        default: // 既定のウィンドウプロシージャ
            return DefWindowProc(hWnd, msg, wParam, lParam);
    }

    return 0;
}

WglWindow* WglWindow::singleton = NULL;

// コンストラクタ
WglWindow::WglWindow(LPCWSTR title, unsigned int width, unsigned int height)
{
    hWnd = 0;
    hDC = 0;
    hRC = 0;
    hFont = 0;
    angle = 0;
    this->text = NULL;
        
    this->width = width;
    this->height = height;
    this->title = new wchar_t [wcslen(title) + 1];
    wcscpy(this->title, title);

    // ウィンドウ作成
    WNDCLASSEX wndCls;
    ZeroMemory(&wndCls, sizeof(wndCls));
    wndCls.cbSize = sizeof(wndCls);
    wndCls.hInstance = (HINSTANCE)GetModuleHandle(NULL);
    wndCls.lpszClassName = this->title;
    wndCls.style = CS_HREDRAW | CS_VREDRAW;
    wndCls.hIcon = LoadIcon(wndCls.hInstance, IDI_HAND);
    wndCls.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    wndCls.hCursor = LoadCursor(wndCls.hInstance, IDC_ARROW);
    wndCls.lpfnWndProc = WndProc;

    // ウィンドウクラスを登録
    int result = RegisterClassEx(&wndCls);

    if(!result)
        throw "登録失敗";

    // デスクトップウィンドウのハンドルを取得
    HWND hDeskWnd = GetDesktopWindow(); 
    RECT rect, deskrc;
    // デスクトップウィンドウの左上と右下の座標を取得
    GetWindowRect(hDeskWnd, (LPRECT)&deskrc);

    // クライアント領域の座標を設定
    long client_width = width;
    long client_height = height;
    rect.right = client_width;
    rect.bottom = client_height;
    rect.top = rect.left = 0;

    // クライアント領域のサイズを指定サイズに設定
    DWORD style = WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_THICKFRAME;
    AdjustWindowRectEx(&rect, style, FALSE, WS_EX_OVERLAPPEDWINDOW);

    // ウィンドウ全体のサイズを求める
    int control_width = (rect.right - rect.left); 
    int control_height = (rect.bottom - rect.top);     
    rect.left = (LONG)((deskrc.right - control_width) * 0.5f); // 左上X座標計算
    rect.top = (LONG)((deskrc.bottom - control_height) * 0.5f); // 左上Y座標計算

    // ウィンドウの作成
    hWnd = CreateWindowEx(
        WS_EX_OVERLAPPEDWINDOW,
        title,
        title,
        style,
        rect.left,
        rect.top,
        control_width,
        control_height,
        NULL,
        NULL,
        wndCls.hInstance,
        NULL);

    // デバイスコンテキスト取得
    hDC = GetDC(hWnd);
    wglMakeCurrent(0, 0);

    // WGL設定
    PIXELFORMATDESCRIPTOR pfd;
    ZeroMemory(&pfd, sizeof(pfd));
    pfd.nSize = sizeof(pfd);
    pfd.cColorBits = 32;
    pfd.cAlphaBits = pfd.cRedBits = pfd.cGreenBits = pfd.cBlueBits = 8;
    pfd.cDepthBits = 24;
    pfd.dwFlags = PFD_DOUBLEBUFFER | PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW;

    int pixel = ChoosePixelFormat(hDC, &pfd);

    if(pixel > 0)
    {
        // 正しいフォーマットを再取得
        DescribePixelFormat(hDC, pixel, sizeof(pfd), &pfd);

        // デバイスコンテキストにピクセルフォーマットを割り当てる
        BOOL enabled = SetPixelFormat(hDC, pixel, &pfd);

        if(!enabled)
           throw exception("失敗");

        // レンダリングコンテキスト作成
        hRC = wglCreateContext(hDC);
        if(hRC == 0)
            throw exception("失敗");

        // これがないと描画できない
        wglMakeCurrent(hDC, hRC);

        // OpenGL初期化
        InitOpenGL();

        // フォント作成
        hFont = CreateFont(
            20,      //フォント高さ
            0,       //文字幅
            0,       //テキストの角度
            0,       //ベースラインとx軸との角度
            FW_REGULAR,     //フォントの太さ
            FALSE,      //イタリック体
            FALSE,      //アンダーライン
            FALSE,      //打ち消し線
            SHIFTJIS_CHARSET,   //文字セット
            OUT_DEFAULT_PRECIS,   //出力精度
            CLIP_DEFAULT_PRECIS,  //クリッピング精度
            ANTIALIASED_QUALITY,  //出力品質
            FIXED_PITCH | FF_MODERN, //ピッチとファミリー
            (LPCWSTR)"alial");     //書体名

        // FPS設定(1000ミリ秒単位で更新)
        Fps::SetUpdateTime(1000);

        // 表示
        ShowWindow(hWnd, SW_SHOW);
        UpdateWindow(hWnd);
    }
}

// デストラクタ
WglWindow::~WglWindow()
{
    wglMakeCurrent(0, 0);

    // 開放
    if(hFont != 0)
        DeleteObject(hFont);

    if(hRC != 0)
        wglDeleteContext(hRC);

    if(hDC != 0)
        ReleaseDC(hWnd, hDC);

    hFont = 0;
    hRC = 0;
    hDC = 0;

    if(title != NULL)
        delete[] title;

    title = NULL;
}

// OpenGLウィンドウ作成
WglWindow* WglWindow::SetOpenGLWindow(LPCSTR title,
                                      unsigned int width,
                                      unsigned int height) {
    if(NULL == singleton)
    {
        try
        {
            singleton = new WglWindow(title, width, height);
        }
        catch(...)
        {
            Release();
            printf("エラー発生");
        }
    }

    return singleton;
}

// OpenGL初期化
void WglWindow::InitOpenGL()
{
    // 閉じるボタン無効化
    //HMENU menu = GetSystemMenu(hWnd, 0);
    //RemoveMenu(menu, SC_CLOSE, MF_BYCOMMAND);

    // 背景色
    glClearColor(0, 0.5, 1.0, 1.0);

    // グラフィック初期化
    // カラーとテクスチャー座標の補間精度
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST);
    
    // アルファチャンネル機能(色設定時の透明度)ON
    glEnable(GL_ALPHA_TEST);
    
    // ブレンディング設定(デフォルトはアルファ合成)
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    /*glEnableClientState(GL_VERTEX_ARRAY);
   glEnableClientState(GL_INDEX_ARRAY);
   glEnableClientState(GL_NORMAL_ARRAY);
   glEnableClientState(GL_TEXTURE_COORD_ARRAY);*/

    // デプスバッファ有効
    glEnable(GL_DEPTH_TEST);

    glEnable(GL_TEXTURE_2D);
    
    // カリング・ライティングモード有効
    glEnable(GL_CULL_FACE);
    glCullFace(GL_FRONT);
    glFrontFace(GL_CCW);
    
    glDisable(GL_LIGHTING);
    glDisable(GL_LIGHT0);
    glDisable(GL_NORMALIZE);

    // シェーディングの設定
    glShadeModel(GL_SMOOTH);
}

// 開放
void WglWindow::Release()
{
    if(singleton != NULL)
        delete singleton;

    singleton = NULL;
}

// メッセージループ処理
bool WglWindow::Loop()
{
    MSG msg; // メッセージ構造体

    // メッセージ処理を先に全て行っておく
    while( PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE) )
    {
        // ウィンドウプロシージャにメッセージを送る
        DispatchMessage(&msg);

        // ウィンドウの終了
        if(WM_QUIT == msg.message)
            return false;
    }

    return true;
}

void WglWindow::Invalidate()
{
    Update();
    Draw();
}

// 更新
void WglWindow::Update()
{
    angle++;
    Fps::Update();
}

// 描画
void WglWindow::Draw()
{
    PAINTSTRUCT paint;

    // 描画開始&画面初期化
    HDC renderDC = BeginPaint(hWnd, &paint);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // カメラ
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0f, (float)width / height, 1.0f, 500.0f);
    gluLookAt(0, -5, 25, 0, 0, 0, 0, 1, 0);

    // ワールド行列
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glRotatef(angle, 0, 1, 0);

    // ポリゴン描画
    glColor3f(1, 0, 0);
    glBegin(GL_TRIANGLES);
    glVertex3f(-1, -1, 0);
    glVertex3f(0, 1, 0);
    glVertex3f(1, -1, 0);
    glEnd();

    // 描画終了&画面更新
    SwapBuffers(hDC);
    EndPaint(hWnd, &paint);
}

WglWindow.SetOpenGLWindowではウィンドウの作成・レンダリングコンテキストの作成・OpenGLの初期化を行っている。Daeモデルを作成して開放するだけの簡単なコードである。

 

こんな簡単なコードなのに何故か失敗してしまう。コンパイルは通るのだが、DAEオブジェクトを呼び出したら「アプリケーションの初期化に失敗しました」とエラーが出てしまう。

 

ここで苦戦して、結局Collada DOMを使うことをやめてしまった。今思えば私の開発環境は「MSVCR90.dll」が入っていなかったのでこれだけ別途用意したのだがおそらく他にも必要なものが不足していたんだろう。VC++2010で組めばよかった。

 

で、Collada DOMをあきらめて他にライブラリがないか探してみた。

 

・FCollada

ColladaのハイレベルAPIを提供。現在公開が終了しているので使えない。

 

・OpenCollada

よくわからない。ドキュメントが皆無。ダウンロードページには「Maya」や「3dsMax」といった項目しかない。Autodeskのプラグイン開発用なのだろうか?

 

SlimDX Collada Loader

DirectX10の機能でColladaを読み込み&描画するAPI。アニメーション読み込みにも対応。スキンメッシュの描画部分は見当たらないが・・・

 

手ごろなもので「SlimDX Collada  Loader」で読み込むことにした。というか他に選択肢がない・・・。

 

ライブラリプロジェクトとサンプルがあったので両方入手。ダウンロードはコチラから。

 

私の環境はXPなのでDirectX10には対応しておらずサンプルを動かせない。幸いにもオープンソースだったので仕方なくライブラリだけDirectX9形式に書き換え、描画周りは結局一から作ることになった。

 

結果はコチラ↓モデルはサンプルのものを使わせていただきました。

 

「F」で面描画、「L」で線描画切り替えが出来るようにしてみた。

DirectX10で描画したらこのようになるらしいです。

Collada(.dae)3DモデルをC#とDirextX10で表示

 

一見うまく言ってるような感じがしますが、他のモデルを読み込むと残念なことになります。例えばCollada変換した「うさうさ」モデル。

これを読み込むとこうなります

これはひどい・・・

 

ちなみにこれは、「ライブラリを改変した状態で描画した」ものです。改変前だと描画すらできません。気になった問題点を幾つか挙げます。

●メッシュに法線又はUVが含まれていないとファイル読み込みに失敗する

法線やUV座標が含まれていることを前提としてるようです。取得に失敗したときの処理がここだけ一切ありません。何故かColoredMeshクラス、TexturedMeshクラスを用意している。そこまで作っておいてなぜUVチェックを怠った?(読み取った法線は描画に一切使われていません)

 

特定のタグが抜け落ちているとファイル読み込みに失敗する

これも法線とUVと同じ理由です。取得に失敗したときの処理がないものに行き当たると失敗します。

 

●マテリアル名=シンボル名でなければマテリアル読み込みに失敗する

マテリアルを取得する際にシンボルIDで 検索せずマテリアルIDで検索してるようです。マテリアルIDとシンボルIDが一致していれば大丈夫ですがそれ以外だと失敗します。


●面情報は「triangles」だけに対応。「polygons」や「polygonlist」は読み込めない。

読み込めるのは三角形面だけで四角形面は読み取り不可。


頂点ウェイト・ボーンバインド行列の探索が甘く、失敗しやすい

「IDに特定の名前を使っていること」が検索の条件みたいです。例えば頂点ウェイトならソースIDに「skin_weights」を含めておくこと。

 

Blender で出力したものを確かめてみると確かにIDに「skin_weights」の文字列が含まれていましたが、そのようなルールはあるのでしょう か?MMDfromColladaで作ったモデルは「skin_weights」が含まれなくてもアニメーション出来るので違うと思うのですが。

 

ここは「WEIGHT」セマンティックと「INV_BIND_MATRIX」セマンティックを検索することで解決しました。

まぁ何やかんや言ってもCollada DOMよりは導入も楽で使いやすいです。ただある程度の改造が必要になります。上記のサイトでも他のモデルを読み込んだら失敗したそうです。

 

今回は息抜き程度の目的だったのでこの辺りでやめましたが、いずれ本格的にCollada読み込みをするときには使わせていただくかもしれません。

 

ただ、中身を見たところAndroidで読み込んだ物とさほどかわりないんですよね。もちろん私は普通のテキストとして、こちらはXml形式で読み込んでいるのでその違いはありますけど。

 

もしかしたら「SlimDX Collada Loader」を改造するより、Androidのコードを改変して自作するほうがいいかもしれない・・・コードを把握してるだけあって実装も速そうだ。

 

結局Collada読み込みは自作する流れになりそうです。無ければ作る!

Collada DOMはVC++2010で再度確認してみようと思います。

 

やってみました→Collada DOMをVC2010で導入したかっただけ