OSX での OpenGL の初期化とか、InterfaceBuilder を用いない OSXアプリケーションの作成とか

ちょいと前に 「OSX での OpenGL の初期化コードを打ってます」みたいなエントリーを書いたんですけど、その続きです.

なにも考えずに OSXOpenGL を使いましょうって場合には、Windows ほどめんどくさいことを考える必要はなくって、Xcode からInterface Builder なり Story Bord なりをつかって OpenGLView をぺとぺとと貼り付けて、その派生クラスなり AppDelegate なりで描画処理を書いてやれば良いだけなんであんまりハマるところは無いかと思うんですが、私の目的地としては iOS / Android / OSXなどで共通利用するためのライブラリ(ミドルウェア)に組み込みたいと考えているものですから、なるべく Xcode なり Interface Builderに依存したくないのです.

いわゆる、 "without Interface Builder" などと海外のサイトで書かれている内容ですね.

個人的な開発環境として、 Xcode 上でコードを打って確認&修正するよりも Emacs 上で完結できた方がやりやすいので、ちょっとしたサンプル程度のアプリケーションであればビルドシステムは CMake で完結してくれた方が都合が良いのです.

(...とはいえ CMake で iOS / Android アプリケーションのビルドシステムを完結させるのは手間が掛かりすぎるので、そのあたりは柔軟にやってるのですが...)

今回やったことの目的としては、

  • InterfaceBuilder を使わずに OSX アプリケーションを作れるようにする
  • CMake で OSXOpenGL アプリケーションを作れるようにする
  • OpenGLView の上に WebView を重ねて表示できるようにする

といった感じです.

OSX アプリケーションに関する日本語情報も少ないことですし、なんとなくやったことをまとめておきたいと思います.

たぶんサマリはこんな感じ

  • 概要
  • サンプルコード
  • InterfaceBuilder を用いない OSX アプリケーションの作成
  • ContentView の設定
  • OpenGLView の初期化
  • OpenGLView の上に WebView を配置する
  • CMakeLists.txt の内容
  • 参考
  • まとめ

...思ったよりも盛りだくさんになってしまいましたが、がんばって行ってみましょう.

概要

iOS / Android / OSX で共通利用できるようなOpenGLライブラリ(ミドルウェア)を作成する場合、 OSX アプリケーションの
作成手順に則って InterfaceBuilder や StoryBoard を利用してアプリケーションを作成すると可搬性が低くなると言う問題点が上がってしまう.

そこで、 Interface Builder を用いずに OSX アプリケーションを作成できるようになりたい.

また、 OpenGL アプリケーションと外部Webサービス(例えば、twittergoogle api)を連携させようとしたとき、
OpenGLView の上に WebView の表示を重ねて認証処理を行いたい.

この時、通常 OpenGLView では、他の View の階層化や重ね合わせ表示が禁止されているため対応処理が必要になった.

さらに、CMake からOSXアプリケーション(Xcode プロジェクト)を生成する場合、適切にバンドルを利用しないと
WebView の挙動やアプリケーションの終了シーケンスが適切に処理されないことがわかった.

ここではこれらの対応についてまとめる.

InterfaceBuilder を用いない OSX アプリケーションの作成

InterfaceBuilder を用いずに OSX アプリケーションを起動させる場合、 NSApplication の設定や Window の設定を
自分で行う必要がある.

以下にコードの抜粋を記す.

int main(int argc, const char * argv[]) {

    [NSApplication sharedApplication];

    // Create a window:

    NSUInteger windowStyle = (NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask);

    NSRect windowRect = NSMakeRect(100, 100, 400, 400);
    NSWindow * window = [[NSWindow alloc] initWithContentRect:windowRect
                                                    styleMask:windowStyle
                                                      backing:NSBackingStoreBuffered
                                                        defer:NO];

    NSWindowController * windowController = [[NSWindowController alloc] initWithWindow:window];

    AppDelegate* app_delegate = [[AppDelegate alloc] init];
    [[NSApplication sharedApplication] setDelegate:app_delegate];
    app_delegate.window = window;

    // TODO: メニューなどはここで追加する(QUIT 等)

    [window orderFrontRegardless];
    [NSApp run];

    return (0);
}

AppDelegate には、 mainWindow プロパティが設定されているが、上記方法で NSApplication に対して AppDelegate を
設定した場合 mainWindow は nil になってしまう点に注意.

(どなたか対応方法を知っていたら教えてください)

AppDelegate から window の操作を行えた方が便利が良いので、ここでは自前で用意した window プロパティに対して
window の設定を行っている.

(OSX アプリケーションの一般知識ではあるが) 表示されているウインドウの [x] ボタンを押してもアプリケーションが
終了するものではないので、必要に応じて TODO と書かれたコメント部分あたりに終了用のメニューを追加しても良い.

サンプルコードでは AppDelegate.mm に以下のコードを追加して最後のウインドウが閉じたときにアプリケーションが
終了するように指定してある.(デフォルト : NO)

- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
{
    return YES;
}

ContentView の設定

window の生成まで行えたので、 生成した Window に対して適切な View を設定すれば表示を行う事ができる.

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    NSRect frame_rect = NSMakeRect(100, 100, 400, 400);

    MyOpenGLView* gl_view = [[MyOpenGLView alloc]initWithFrame:frame_rect];

    // View をオーバーレイさせるときには setWantsLayer に YES を指定
    [gl_view setWantsLayer:YES];
    [self.window setContentView:gl_view];

// 以下略

手抜きをして、View のサイズがハードコーディングになっているが、Windowのサイズから View のサイズを決定しても良い.

単に OpenGLView を利用する場合、 "MyOpenGLView" の部分を "OpenGLView" と直接指定してしまっても良いが、ここでは
OpenGL 4.x を利用したかった点と処理の書きやすさから派生クラスの "MyOpenGLView" を用意した.

OpenGLView の初期化

OpenGLView をそのまま利用すると、 OpenGL 2.x の固定パイプラインしか利用できないため、OpenGL 4.x を利用できるように
初期化処理を行う.

- (id)initWithFrame:(NSRect)frame_rect
{

    static const NSOpenGLPixelFormatAttribute attr[]= {
        NSOpenGLPFANoRecovery,
        NSOpenGLPFADoubleBuffer,
        //NSOpenGLPFAWindow,
        NSOpenGLPFAAccelerated,
        NSOpenGLPFAColorSize,  24,
        NSOpenGLPFAAlphaSize,   8,
        NSOpenGLPFADepthSize,  24,
        NSOpenGLPFAStencilSize, 8,
        NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
        0
    };

    NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
    self = [super initWithFrame:frame_rect pixelFormat:pixelFormat];

    if (self != nil) {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(_surfaceNeedsUpdate:)
                                                     name:NSViewGlobalFrameDidChangeNotification
                                                   object:self];

        timer_ = [NSTimer timerWithTimeInterval:1.0/60.0
                                         target:self
                                     selector:@selector(update)
                                       userInfo:self
                                        repeats:true];
        if (timer_) {
            [[NSRunLoop currentRunLoop] addTimer:timer_ forMode:NSRunLoopCommonModes];
        }
    }
    return self;
}

NSOpenGLPixelFormat に NSOpenGLPFAOpenGLProfile 指定することで利用するOpenGLプロファイルを選択できるようだが、
現状(OSX : 10.10.3 / Xcode : 6.3.2)では、 NSOpenGLProfileVersion3_2Core を指定するか無指定(GL 2.1 core)しか
選択できないようだ.

3.2 Core と書かれているがチップセットが対応していれば 4.x プロファイルの利用も行える.

(shader の文法からざっくり分類してしまうと OpenGL は 2.x系と 3.x系に分かれるので、3.2 later であることが重要
と考えられているからじゃなかろうかと推察するが詳細はしらん)

余談だが、初期化後に OpenGL のプロファイルを確認したければ

    NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
    self = [super initWithFrame:frame_rect pixelFormat:pixelFormat];

    NSOpenGLContext* context = [self openGLContext];
    [context makeCurrentContext];

    printf("GL_VERSION: %s\n", glGetString(GL_VERSION));
    printf("GL_RENDERER: %s\n", glGetString(GL_RENDERER));
    printf("GL_VENDOR: %s\n", glGetString(GL_RENDERER));
    printf("GL_SHADING_LANGUAGE_VERSION: %s\n", glGetString(GL_SHADING_LANGUAGE_VERSION));

とでもすれば良い

OpenGLView の上に WebView を配置する

「ContentView の設定」の章でさらっと流してしまったが、OpenGLView の上に WebView 等の他の View を表示しようと
しても期待通りの表示結果にならない.

CAOpenGLLayer などを用いると一度バックグラウンド描画を行ってから OpenGL の描画結果を View に表示することが
できるようだ.

これには OpenGLView の setWantsLayer に YES を設定すれば良いらしい.

    MyOpenGLView* gl_view = [[MyOpenGLView alloc]initWithFrame:frame_rect];

    // View をオーバーレイさせるときには setWantsLayer に YES を指定
    [gl_view setWantsLayer:YES];

詳しい方がいれば詳細を教えていただきたいのだが...

推察するに setWantsLayer を指定するとオフスクリーン描画が行われた後に、View に レンダリング結果を描画という
手順の挙動をするのでは無いかと思う.

OSX において OpenGL の初期化方法は他にも OpenGLContext を作成しておき別途用意した NSView(の派生クラス)
の描画時にでも OpenGLContext に setView するという方法もある.

この場合 CAOpenGLLayer などとの関連処理が行われないためか、 setWantsLayer を YES と指定すると abort してしまう.

OpenGLView は、単に OpenGLContext と NSView を関連づけるだけの役割があるだけでなく、 CAOpenGLLayer もしくは
その類似処理との協調処理を行うではないかと推察している.

ここからパフォーマンスを優先するのか、他 View との協調動作を求めるのかで処理を使い分けた方が良いのかもしれない.

(詳しい人教えてください)

CMakeLists.txt の内容

上記説明までで interface builder を用いること無く OSX アプリケーションを作成できるのだが、
CMake を用いる場合、単に ADD_EXECUTABLE の指定をすると ウインドウ関連処理が期待通り動作しない.

具体的には、 WebView から WebView 内のテキストボックスに文字入力を行おうとしても、別ウインドウにキー入力が奪われたり、
終了後に別ウインドウへのフォーカスが適切に動作しない等の問題が起こる.

また、 CMake に限らず Xcode のプロジェクトを生成するときに "Command Line Tool" などを選択してプロジェクトを生成し
上記コードを実行する際にも同様の問題が発生する.

調べていくと、OSXのバンドル形式に則っていない OSX アプリケーションは、OS側と協調動作を行わないため
このような現象が起こると言うことなので、CMake では MACOSX_BUNDLE オプションを追加し、直接実行ファイルを生成
するのではなく バンドルを生成するように指定する.

add_executable(${TARGET_NAME} MACOSX_BUNDLE ${SRC_FILES})

バンドルでは plist を適切に設定する必要があるが、そのあたりは CMake がよろしくやってくれるのであまり気にする
必要は無い.

plist の内容を変更したい場合には、"Info-CMake.plist" のようなテンプレートを用意しておき、
以下のように指定を行えば良い

set(MACOSX_BUNDLE_EXECUTABLE "${TARGET_NAME}")
set(MACOSX_BUNDLE_INFO_STRING "${TARGET_NAME}")
set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.example")
set(MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_NAME} Version ${VERSION}")
set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME})
set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${VERSION})
set(MACOSX_BUNDLE_BUNDLE_VERSION ${VERSION})
set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2013.")
set(MACOSX_BUNDLE_NSMAIN_NIB_FILE "")
set(MACOSX_BUNDLE_NSPRINCIPAL_CLASS "NSApplication")

set_target_properties(${TARGET_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST Info-CMake.plist)

参考

○ "NSTextField とか重ねて表示できる OpenGL の View を実装するには"

http://qiita.com/kbinani/items/646bf875f52148f798a2

最初、「OpenGLView に addSubView するだけだろ」なんて事を考えながらサンプルを作っていたのですが、
どうにも上手く表示が行えずにハマっていました.

上記エントリーでは CAOpenGLLayer のサブクラスを作成して...といった事をやっているのですが、
私には必要性が理解できませんでした.

ただ、サンプルを作成する際の足がかりとしてはとても参考にさせていただきました.


○ "OSX/Cocoa window without XCode and Interface Builder"

http://blog.glampert.com/2012/11/osxcocoa-window-without-xcode-and.html

サンプルを書き始めたとき、「NSApplication#sharedApplication に対して AppDelegate を設定すれば
たぶん InterfaceBuilder を使わなくても大丈夫だよな?あとは NSWindow とか NSView とかを初期化
していけば IB なんていらねぇだろ...」なんて考えてました.

最終的には、バンドルを使わないとアプリケーションの挙動がおかしくなるという結論にたどり着くことになるんですが、
どうもアプリケーションの挙動がおかしいので方々を調べていったところ上記エントリーを見つけました.


私が元々書いていたコードでは、AppDelegate の初期化時に Window を作って addSubView して...といった
感じでコードがぴよぴよしてたんですけど、上記エントリーを見て「こうやって書くとすっきりするのかっ」と
感じました.

結局、アプリケーションの挙動を適切な形に治すという意味では役に立っていませんが、without Interface Builder
という観点で参考にさせていただきました.

○ "LayerBackedOpenGLView"

https://developer.apple.com/library/mac/samplecode/LayerBackedOpenGLView/Introduction/Intro.html

Apple のサンプルです.

まとめ

かなり長編のエントリーになってしまいました.
ずいぶんと OSX アプリケーションに関する知識も付いてきた感じがするので、そろそろ本来やりたかったことに
本腰を入れようかと思います.