Xcode/Cocoaで始めるモダンOpenGL

モダンなOpenGLを勉強したいと思い立ったので、「OpenGL 4.0 シェーディング言語 実例で覚えるGLSLプログラミング」(原著は OpenGL 4 Shading Language Cookbook)という本で少しずつ勉強している。

OpenGL自体はグラフィックスに関するAPIなので、アプリケーションやウインドウの初期化、画像の読み込み等は各プラットフォームで使えるGUIツールキットの力を借りることになる。この本のサンプルコードではQt(キュート)を使っているが、GitHubにあるサンプルコードではGLFWを使っている。

筆者としては、Cocoaで書いているMac向けのアプリケーションでOpenGLを使いたいという目標があるため、サンプルコードもCocoaアプリケーションに組み込む形で試すことにした。

筆者の環境は macOS Sierra, Xcode 8.0 である。

Xcodeでプロジェクトを作る

まずは、普通に Cocoa Application のプロジェクトを作る。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-0028-10-22-18-02-13 %e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-0028-10-22-18-02-49 ターゲット設定の Linked Frameworks and Libraries のところに OpenGL.framework を追加する。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-0028-10-22-18-04-08

なお、C++でもGLSLライクなベクトル・行列演算ができるように、GLMを用意しておく。筆者はMacPortsでGLMをインストールしたので、/opt/local/include をインクルードパス(User Header Search Paths)に追加した。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-0028-10-22-23-01-52

NSOpenGLView を継承したビューのクラスを作る。名前は適当に MyGLView とした。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-0028-10-22-23-03-09

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-0028-10-22-23-03-46GLM 等の C++ のライブラリを使いたい場合は、 MyGLView.m の名前を MyGLView.mm に変えておく。

MainMenu.xib を開いて、メインウインドウに NSOpenGLView を貼り付ける。貼り付けたら、 右側の Identity inspector → Custom Class の Class を MyGLView に変える。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-0028-10-22-23-08-24ちなみに、Attributes inspector では OpenGL の初期化に関する各種パラメーターを設定できる。しかし、プロファイルは選択できない。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-0028-10-22-23-10-47ちなみに、ビューをRetina対応させたい場合は Resolution [ ] Supports Hi-Res Backing にチェックを入れればいいらしい。プログラム的にやりたい場合はビューの wantsBestResolutionOpenGLSurface プロパティーが関係する。

NSOpenGLViewの初期化

各種バッファのサイズや OpenGL プロファイルを指定するためには、 NSOpenGLView の初期化の際に NSOpenGLPixelFormat オブジェクトを与えてやる。MyGLView オブジェクトがプログラム的に初期化されるのであれば -init 系のメソッドをどうこうしてやれば良いが、今回はNIBファイルから構築されるので、 -awakeFromNib メソッドで -setPixelFormat: を呼んでやらないと反映されない。

@implementation MyGLView

+ (NSOpenGLPixelFormat *)myPixelFormat
{
    static const NSOpenGLPixelFormatAttribute attributes[] = {
        NSOpenGLPFAColorSize, 24,
        NSOpenGLPFADepthSize, 16,
        NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
        0
    };
    NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
    return pixelFormat; // 最近の Xcode ではデフォルトで ARC を使うようなので autorelease は不要
}

- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame pixelFormat:[MyGLView myPixelFormat]];
    if (self) {
    }
    return self;
}

- (void)awakeFromNib
{
    [self setPixelFormat:[MyGLView myPixelFormat]];
}

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];
    
    // Drawing code here.
}

@end

参考:OpenGL Programming Guide for Mac: Choosing Renderer and Buffer Attributes — OpenGL Profiles (OS X v10.7)

シェーダーの読み込み

GLSLで書かれたシェーダーのソースコードは、C++のソースコードに埋め込むこともできるが、編集のことを考えると個別のファイルに保存してアプリケーションバンドル内から読み込むのが適当だろう。

Xcode では、New File → Empty を選択して空のファイルを作る。名前は basic.vert とする。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-0028-10-22-23-59-07 %e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-0028-10-22-23-59-44basic.frag も同様に作る。

シェーダーの読み込み等、OpenGLのAPIを使った初期化の処理は -prepareOpenGL メソッドに記述すれば良いだろう。

@implementation MyGLView
{
    GLuint programHandle;
}

// 中略(さっき載せた初期化コード)

- (GLuint)compileShader:(NSString *)name ofType:(NSString *)type shaderType:(GLenum)shaderType
{
    GLuint shader = glCreateShader(shaderType);
    if (shader == 0) {
        NSLog(@"failed to create the shader (%@)", name);
        return 0;
    }

    NSString *s = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:type] encoding:NSUTF8StringEncoding error:NULL];
    if (s == nil) {
        NSLog(@"failed to load the shader from file (%@.%@)", name, type);
        return 0;
    }
    const GLchar *shaderCode = [s UTF8String];
    const GLchar *codeArray[] = {shaderCode};
    glShaderSource(shader, 1, codeArray, NULL);

    glCompileShader(shader);

    GLint result;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &result);
    if (result == GL_FALSE) {
        NSLog(@"failed to compile the shader (%@)", name);

        GLint logLen;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLen);
        if (logLen > 0) {
            char *log = (char *)malloc(logLen);
            GLsizei written;
            glGetShaderInfoLog(shader, logLen, &written, log);
            NSLog(@"shader (name:%@, type:%@) log: %s", name, type, log);
            free(log);
        }
        return 0;
    }
    return shader;
}

- (void)prepareOpenGL
{
    NSLog(@"renderer: %s, vendor: %s, GL version:%s, GLSL version: %s", glGetString(GL_RENDERER), glGetString(GL_VENDOR), glGetString(GL_VERSION), glGetString(GL_SHADING_LANGUAGE_VERSION));

    GLuint vertShader = [self compileShader:@"basic" ofType:@"vert" shaderType:GL_VERTEX_SHADER];
    if (vertShader == 0) {
        return;
    }

    GLuint fragShader = [self compileShader:@"basic" ofType:@"frag" shaderType:GL_FRAGMENT_SHADER];
    if (fragShader == 0) {
        return;
    }

    programHandle = glCreateProgram();
    if (programHandle == 0) {
        NSLog(@"failed to create program");
        return;
    }

    glAttachShader(programHandle, vertShader);
    glAttachShader(programHandle, fragShader);

    glLinkProgram(programHandle);

    // 略
}

- (void)drawRect:(NSRect)dirtyRect
{
    [super drawRect:dirtyRect];

    [[self openGLContext] makeCurrentContext];

    glClearColor(0.0, 0.3, 0.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Drawing code here.
    // 略

    glFlush();
    [[self openGLContext] flushBuffer];
}

@end

画像の読み込み

テクスチャ等に使う画像を読み込む方法について。

Cocoa の流儀で行くと、画像ファイルは NSImage クラスで読み込むのが自然だろう。

NSImage オブジェクトから実際のデータ(バイト列)を得るには、

  1. NSImage -representations メソッドで NSImageRep の配列を得る
  2. その中から NSBitmapImageRep のインスタンスを探す
  3. NSBitmapImageRep -bitmapFormat で、フォーマットが望みのものであることを確認する
  4. NSBitmapImageRep -bitmapData でデータへのポインタを取得する

という方法が考えられる。ただ、これでは望みのフォーマットのデータが得られるかは運任せになる。その辺をちゃんとやろうとするなら、 NSBitmapImageRep -initWithBitmapDataPlanes:pixelsWide:pixelsHigh:bitsPerSample:samplesPerPixel:hasAlpha:isPlanar:colorSpaceName:bitmapFormat:bytesPerRow:bitsPerPixel: を使って自分で NSBitmapImageRep を構築し、そこに画像を描画してやる、という形になるだろうか。

もっといいやり方があるかは知らない。