モダンな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 のプロジェクトを作る。
ターゲット設定の Linked Frameworks and Libraries のところに OpenGL.framework を追加する。
なお、C++でもGLSLライクなベクトル・行列演算ができるように、GLMを用意しておく。筆者はMacPortsでGLMをインストールしたので、/opt/local/include をインクルードパス(User Header Search Paths)に追加した。
NSOpenGLView を継承したビューのクラスを作る。名前は適当に MyGLView とした。
GLM 等の C++ のライブラリを使いたい場合は、 MyGLView.m の名前を MyGLView.mm に変えておく。
MainMenu.xib を開いて、メインウインドウに NSOpenGLView を貼り付ける。貼り付けたら、 右側の Identity inspector → Custom Class の Class を MyGLView に変える。
ちなみに、Attributes inspector では OpenGL の初期化に関する各種パラメーターを設定できる。しかし、プロファイルは選択できない。
ちなみに、ビューを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
シェーダーの読み込み
GLSLで書かれたシェーダーのソースコードは、C++のソースコードに埋め込むこともできるが、編集のことを考えると個別のファイルに保存してアプリケーションバンドル内から読み込むのが適当だろう。
Xcode では、New File → Empty を選択して空のファイルを作る。名前は basic.vert とする。
シェーダーの読み込み等、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 オブジェクトから実際のデータ(バイト列)を得るには、
- NSImage -representations メソッドで NSImageRep の配列を得る
- その中から NSBitmapImageRep のインスタンスを探す
- NSBitmapImageRep -bitmapFormat で、フォーマットが望みのものであることを確認する
- NSBitmapImageRep -bitmapData でデータへのポインタを取得する
という方法が考えられる。ただ、これでは望みのフォーマットのデータが得られるかは運任せになる。その辺をちゃんとやろうとするなら、 NSBitmapImageRep -initWithBitmapDataPlanes:pixelsWide:pixelsHigh:bitsPerSample:samplesPerPixel:hasAlpha:isPlanar:colorSpaceName:bitmapFormat:bytesPerRow:bitsPerPixel: を使って自分で NSBitmapImageRep を構築し、そこに画像を描画してやる、という形になるだろうか。
もっといいやり方があるかは知らない。