黒画面への道(CocoaでOpenGL)


やあ子供たち。今日はMacOSX 10.7.4上で、XCode 4.3.2を使ってのOpenGL黒画面を出すまでの道を端的にメモしていくぞ。(後日記:MaxOSX 10.8.2上の、XCode 4.5.2でも本記事の操作説明は有効であることを確認したよ)まとめてくれてるページとかいくつかあるけどおじさんも自分なりにまとめておきたいものだと兼ねてから考えていたよ。ではまいろうか。
●NSOpenGLViewを使わない道を選択しよう
NSOpenGLViewを使う方法と、使わない方法がある。
どちらでいくか好みの問題だが、本メモはNSOpenGLViewを使わない方法についてのものだよ。ここにも書いてあるように、せっかく勉強するのであればちゃんとNSOpenGLContextを自分でNSViewに関連付けるやり方を覚えたほうがよさそうだ。NSOpenGLViewの使い方についてはここでは書いてないのでそれを知りたいという人はここでもう帰ってくれ。ここには情報はないよ。
Cocoaアプリケーションを作ろう
XCodeでCocoaApplicationを作成しよう。アプリ名を入力するダイアログの中に、「Create Document-Based Application」というのに自動でチェックがついてるので今回はこれを外そう。別に複数ドキュメントが扱えるアプリを作るわけではないからね。
OpenGLフレームワークをリンクしよう
まずはOpenGL.Frameworkを、プロジェクトの設定の Build Phases タブのLink Binary with Libraries の方から追加。
●インタフェースビルダー(IB)でカスタムビューを作成しよう
MainMenu.xib ファイルを開き、メインメニューではなく、Windowオブジェクトを表示させた状態で、このダイアログウィンドウの中に対し、XCode右サイドペインのUtilitiesの下半分にある、ObjectLibraryの方から、CustomViewをメインウィンドウのなかにドロップ。(これがアルファベット順とかになってなくて探しにくいんだけどね!)あとは位置やサイズを望むように編集しよう。
●カスタムビューコードの作成
次に今配置したCustomViewを選択した状態で、XCode画面右側Utilitiesペインの上半分、Identity Inspector の中で名前をMyGLView(←任意の名前。例えばこういう名前)にした後、これと同じ名前のObj-C Class作成する。
NSViewをサブクラス化したMyGLViewを作成。MyGLView実装部のファイル拡張子を.mから、.mmにする。これでC++が使えるようになる。
●カスタムビューコードの記述ー黒画面最小コード
さてさて、黒画面に出会うための、最終的に必要な最低限のコーディングはこの自分用に作成したMyGLViewカスタムビューのインタフェース部(.h)と実装部(.mm)であり、以下のようになるよ。
MyGLView.h

#import <Cocoa/Cocoa.h>
#import <OpenGL/OpenGL.h>// を追記
#import <OpenGL/gl.h>// を追記
@interface MyGLView : NSView
{
    NSOpenGLContext* _context;// を追記
}
@end

MyGLView.mm

#include <iostream>
#import "MyGLView.h"

@implementation MyGLView

- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        if(self == nil)
            return nil;        
        // コンテキストの作成
        {
            NSOpenGLPixelFormatAttribute att[] = 
            { 
                NSOpenGLPFAWindow, 
                NSOpenGLPFADoubleBuffer, 
                NSOpenGLPFAColorSize, 24, 
                NSOpenGLPFAAlphaSize, 8, 
                NSOpenGLPFADepthSize, 24, 
                NSOpenGLPFANoRecovery, 
                NSOpenGLPFAAccelerated, 
                0 
            };
            NSOpenGLPixelFormat* pixelFormat = 
            [[NSOpenGLPixelFormat alloc] initWithAttributes:att];
            _context = [[NSOpenGLContext alloc] initWithFormat: pixelFormat shareContext: nil];
        }
        [_context makeCurrentContext];
        // シェーダーのセットアップなんかが必要ならここでやったり
        // その他必要な、オブザーバーへの自身の登録などあればここで
        // 以下ではバックグラウンドカラーの設定などしてみてる
        glClearColor (0.0, 0.0, 0.0, 0.0);// 今回は黒画面なので(^^;
        glClearDepth( 1.0 );
    }  
    return self;
}

- (void)lockFocus
{  
    [super lockFocus];
    // コンテキストのビューポートとなるビューを自身にセット
    if ([_context view] != self) {
        [_context setView:self];
    }    
    // コンテキストをカレントにする。
    //(これはdrawRectの冒頭にあってもよい)
    [_context makeCurrentContext];
}

- (void)drawRect:(NSRect)dirtyRect
{
    // Drawing code here.
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    // glFlush(); // 下記flushBufferの中で呼ばれるそうなので必要なし。
    // 
    // <描画コード>    
    //
    // ダブルバッファの表面と裏面のスワップを行う。
    [ _context flushBuffer ];  
}
@end

以上の内容の説明は以下になるよ。
●インタフェース部
NSOpenGLContext*をメンバ変数に加える。もうこれに尽きるね。余談だけどコンテキストって、OpenGLの静的状態を覚えたりもしてる実体でもあるらしいのね、ふーんそうなんだ。まあビューにOpenGLをくっつけるわけだけど、コンテキストはそのOpenGLさん本体なんだって思ってればいいよ。OpenGLさんにはビューに起きた変化を通知しなくてはならなかったりもするんだけど(後述)、とにかくOpenGLさんの実体がこのコンテキストだと思えばいいやな。
●initWithFrame() の中を記述。
コンテキストを作成。そのためにピクセルフォーマットを作成する必要があるぞ。上記例ではダブルバッファの指定もしている。
●lockFocus() の中を記述。
 view lockFocusは、drawRectに先立って呼ばれる、ビューを描画対象にするためのメソッド。ここではsetView()メソッドを使ってコンテキストの描画対象(ビューポート領域)となるビューを自身に設定している。でー、これがどうも必要なんだね。Windowsの場合はHGLRCというOpenGLレンダリングコンテキストを作成する際に描画対象領域のデバイスコンテキストを渡す必要があるのでコンテキストと描画対象領域とのリンクはそこでとっているのだろうけど、Cocoaの場合はここ以外、描画対象領域を指定する場所が他にどこにもにないからね。これはWindowsの場合にはない、必要操作かと。なお上記コードでは、ついでにこの中でGLコンテキストをカレントにしてしまうのら。
●drawRect()
コンテキストをカレントにするのはここの冒頭でもよいという噂。あとは必要な描画コードをここに書きまくろう。なお、ダブルバッファを有効化している場合は、描画が終わった一番最後で、表裏の切り替えを忘れずにやろう。(裏表の切り替え←正確には、バックバッファの内容をフロントバッファにコピーしてくることだよ。)
●まとめ
実際の作業内容という観点で見ると、結局、

  • IB:カスタムビューを貼りつけて、それに好きなクラス名をつける。
  • プロジェクトナビゲーター:同じクラス名のObjCクラスを作成し
  • OpenGLフレームワークを足してopengl/gl.hをincludeすること
  • そのインタフェース部、および実装部に、上記固定メソッドを実装。

という感じになるかと。
●おまけ(ウィンドウのリサイズ時処理)
さて、おまけだよ。ここでは、ウィンドウがリサイズした時の描画更新で、やはりCocoaでのOpenGL実装独特の掟があるのでここで触れておこう。ユーザーによりウィンドウのサイズが変更されると、ウィンドウのリサイズイベントが発生する。まずはそのイベントの観察者(リスナー)として、システムの通知センターに自らを登録しよう。以下のコードを一行、initWithFrameの中に書けばいいぞ。

  // observer
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(windowResized:)
                                               name:NSWindowDidResizeNotification
                                             object:[self window]];       

さて、あとはイベントがきた際に呼び出されるwindowResizedメソッドを実装していけばいいのだが、そこでは何をするだろうか。まあWindowsでのOpenGLプログラミングならここで、ビューポートの再設定やビューマトリクスの再調整などするのだろう。それはそうだ。それは同じだからコンテキストをカレントにするのを忘れずに、いくらでもそういう処理を書けばいいのだが、それだけじゃだめだ。だめ。それだけじゃちゃんと描画更新されないよ。何故だろう何が必要なのだろう。そう、それ以前に必要なことがあるんだ。それは、ここにも書いてあるんだが、コンテキストの更新というものをしなくてはならないんだね。以下のようなコードが最低限必要になる。

- (void)windowResized:(NSNotification *)notification;
{
    float w = self.bounds.size.width;
    float h = self.bounds.size.height;
    [ _context update ]; // コンテキストの更新!
    [ _context makeCurrentContext ];
    glViewport( 0, 0, w, h );
    return;
}

素直に考えるとこれをやって初めてOpenGLに、ビューのサイズが変わりましたよという情報の更新がかかるのかな。まあこれも、本文で書いたように、ビューとOpenGLコンテキストとが疎結合になっているからならではの事情だと理解しよう。
●おまけのおまけ(マウス移動のコールバック処理)
おまけのおまけは、マウス移動のコールバックをどうやって受け取るかだ。これはもうググれば語り尽くされた感がある内容になってしまうけど、都度ググるのもつかれてしまうのでここにまとめてメモしておくぞ。
マウスのコールバックを受け取るには上記のMyGLView において、ただmouseMoved()関数をオーバーライドしただけではだめで、以下の初期化処理が必要だ。

- (void)awakeFromNib
{   
    //mouseMoveを取得する設定
    // for capturing mouseMove
    [[self window] makeFirstResponder: self];
    [[self window] setAcceptsMouseMovedEvents: YES];
    return;
}

その上で、肝心のコールバックは以下のようにする。

- (void)mouseMoved:(NSEvent*) event
{
    // 左下原点の座標を、標準出力に表示します。
    NSPoint p = [event locationInWindow];
    p = [self convertPoint:p fromView:nil];
    std::cout << "mouseMoved" << p.x <<", "<< p.y<<std::endl;
    // ここで何か描画情報を更新
    // 再描画をかける
    [ self setNeedsDisplay:YES ];
    return;
}

これで、ビューの左下を原点としたときの、マウス移動した座標がとれる状況になるよ。
ここで気をつけたいのは最後の setNeedsDisplay:YES だ。これはWindowsプログラミングにおけるInvalidate()のようなもので、ビューに再描画の必要性を通知するものらしい。これがないと、マウスで描画情報(drawRectで参照されている)に何か変更を加えても、再描画がかからないぞ。
じゃあ今回はこの辺で。チャオ!