モダンな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 を構築し、そこに画像を描画してやる、という形になるだろうか。
もっといいやり方があるかは知らない。





