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


![iwantthis.png](https://qiita-image-store.s3.amazonaws.com/0/49610/a9d54133-33ec-df70-19eb-7f9db5053e45.png "iwantthis.png")


　上図のサンプルのように OpenGL で描画した View の上にテキストボックスを表示させたい場合、`CAOpenGLLayer` を使うとうまくいく。

　OpenGL の `glClearColor` で背景を半透明に設定したいときの注意点についてもメモしてある。

# 背景

* InterfaceBuilder の Object Library にある `NSOpenGLView` は、子要素 (`NSButton`など) を持たせられないようになっている。
* `NSButton` などを子要素とせずに `NSOpenGLView` の上に重ねるように設置したとしても、OpenGL の描画で `NSButton` などの描画が潰れてしまう。
* `NSOpenGLView` の代わりに、`NSOpenGLView` オーバーライドした `NSView` を使って子要素を持たせられるようにしても、結局 `NSButton` などの描画が潰れてしまう。
* 具体的には下図のような描画になってしまう。
 * テキストボックスが裏に隠れているように見えるが、実際にはテキストボックスは OpenGL サーフェスの手前に出ており、描画はされていないものの文字は入力できる状態。
![bad.png](https://qiita-image-store.s3.amazonaws.com/0/49610/ceb13f9c-fbd0-b857-5cb8-93d93910acda.png "bad.png")

# 解決方法

## 方針

* `NSView` をオーバーライドしたカスタム View クラスを作り、この View の BackingLayer で OpenGL の描画処理をやる。
* BackingLayer には、`CAOpenGLLayer` を拡張したカスタム Layer クラスを使う。

## サンプル実装

```objc

/* 指定した opacity の背景の上に、glut の teapot を表示するコードを何処かで実装する */
void DrawTeapotWithBackgroundOpacity(GLfloat opacity);


@interface LayerBackedGLView : NSView
{
}
@end


@interface CustomOpenGLLayer : CAOpenGLLayer
{
}
@end


@implementation CustomOpenGLLayer

-(BOOL)canDrawInCGLContext:(CGLContextObj)ctx
               pixelFormat:(CGLPixelFormatObj)pf
              forLayerTime:(CFTimeInterval)t
               displayTime:(const CVTimeStamp *)ts
{
	return YES;
}

-(void)drawInCGLContext:(CGLContextObj)ctx
            pixelFormat:(CGLPixelFormatObj)pf
           forLayerTime:(CFTimeInterval)t
            displayTime:(const CVTimeStamp *)ts
{
    CGLSetCurrentContext(ctx);
    DrawTeapotWithBackgroundOpacity(0.5);
    [super drawInCGLContext:ctx
                pixelFormat:pf
               forLayerTime:t
                displayTime:ts];
}

-(BOOL)isOpaque
{
    /* glClearColor で半透明な背景色を指定している場合、YES を返すとうまくいかない。 */
    return NO;
}

@end


@implementation LayerBackedGLView : NSView

-(id)initWithFrame:(NSRect)frameRect
{
    self = [super initWithFrame:frameRect];

    /* ここを YES にすると、layer が必要になったタイミングで makeBackingLayer が呼ばれる */
    [self setWantsLayer:YES];

    /* Retina 対応. 以下を参照
       https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSViewOpenGLAdditions
    */
    [self setWantsBestResolutionOpenGLSurface:YES];

    return self;
}

-(CALayer*)makeBackingLayer
{
    CustomOpenGLLayer* layer = [[CustomOpenGLLayer alloc] init];
    [layer setNeedsDisplayOnBoundsChange:YES];
    return layer;
}

-(BOOL)isOpaque
{
    /* glClearColor で半透明な背景色を指定している場合、YES を返すとうまくいかない。 */
    return NO;
}

@end

```

# 注意点

## OpenGL の描画で、背景色を半透明にしたい時

* サンプル実装のコードコメントにあるように、カスタマイズした `NSView`、`CAOpenGLLayer` の `isOpaque` 関数をオーバーライドしている場合、どちらも `YES` を返してはいけない。
* `isOpaque` をオーバーライドしていない場合は特に何もしなくて OK。

## Mac OSX Yosemite

* `CAOpenGLLayer` を継承した `NSOpenGLLayer` というクラスがあるが、こちらを使うと Yosemite で期待したとおり動かなかった。

# 参考

* [LayerBackedOpenGLView](https://developer.apple.com/library/mac/samplecode/LayerBackedOpenGLView)
 * Apple による LayerBacked 描画のサンプル。
 * OpenGL のサーフェスの上に UI 部品を乗せる場合は Layer-Backed 描画にするといいよ、と書いてある。
* [Layer-backed OpenGLView redraws only if window is resized](http://stackoverflow.com/questions/7610117/layer-backed-openglview-redraws-only-if-window-is-resized/11213382#11213382)
 * 最も参考になった StackOverflow の問答
* [NSOpenGLLayer Class Reference](https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSOpenGLLayer_Class/Reference/Reference.html)