高度な検索
Developer Connection
Member Login ログイン | ご入会 ADC連絡先

Technical Q&A QA1209
Updating OpenGL Contexts


Q: OpenGL コンテキストはいつ、なぜ更新する必要があるのでしょうか?

A: OpenGL アプリケーションは、ドローアブルのジオメトリが変わったときや、レンダラが変わったときに OpenGL コンテキストを更新する必要があります。ウインドウ化されたアプリケーションの場合、更新は通常、提供されている更新 API の aglUpdateContext または NSOpenGLContext の -update メソッドを通じて実行されます。フルスクリーンクライアントの場合は、コンテキストの更新を実行するには、更新関数を呼び出す代わりに、CGLSetFullScreen または aglSetFullScreen の呼び出しを再発行する必要があります。全体として、これらの関数によって OpenGL エンジンはサーフェイスサイズが設定され、仮想スクリーンの変更に合わせてレンダラが正しく更新されることを保証できます。コンテキストの更新に失敗すると、レンダリングが不自然になるか、OpenGL 出力が完全になくなることがあります。最後に、コンテキスト更新 API の使用の負担は小さくないので、使い過ぎないようにします。クライアントは、フレームごとに更新を発行するのではなく、システムレベルのイベントと通知に応じて適切なコンテキスト更新の呼び出しを行うことを強くお勧めします。

また、ドローアブルのジオメトリまたは現在のレンダラに影響を与えるアクションのあとにも、コンテキストを更新する必要があります。システムイベントでは、ドローアブルの移動、リサイズ、座標オフセット、およびディスプレイ設定の変更(ディスプレイの深さの変更)がそうしたアクションに相当します。これらのイベントの処理方法は、使用しているクライアント API によって決まります。NSOpenGLView をサブクラス化している Cocoa アプリケーションは、NSOpenGLView の -update メソッドを自動的に呼び出します。GLUT フレームワークではコンテキストの更新が正しく処理されることが保証されているため、これは GLUT アプリケーションにも適用されます。Cocoa で書かれているけれども NSOpenGLView をサブクラス化していないクライアントは、NSOpenGLContext の -update メソッドを直接使用する必要があります。AGL API を使った、ウインドウ化されている Carbon アプリケーションでは、aglUpdateContext を呼び出す必要があります。フルスクリーンの CGL および AGL アプリケーションの場合は、クライアントは更新関数を呼び出すのではなく、それぞれ CGLSetFullScreenaglSetFullScreen を再発行する必要があります。これらは、深さ、サイズ、ディスプレイ設定の変更を反映させるために必要です。これらの更新呼び出しは、更新を必要とするシステムイベントのあと、クライアントがコンテキストへの描画を実行する前に行う必要があります。最後に、クライアントの OpenGL コンテンツを新しいドローアブルのサイズに合わせて正しく拡大/縮小するためにドローアブルがリサイズされる場合は、glViewport コマンドを発行できます。ここでは、それぞれのケースごとにコード例を見ていくのが役に立ちます。

注:
コンテキストの更新を必要とするこれらのシステムレベルイベントの中には、コンテキストのバッファを割り当て直すものがあるため、すべてのコンテキストを更新した後で、シーン全体を再描画する必要があります。



Cocoa の NSOpenGLView

NSOpenGLView をサブクラス化している Cocoa アプリケーションでは、処理する必要のある 4 つのすべてのケースにおいて、NSOpenGLView の -update メソッドが自動的に呼び出されます。クライアントがこのメソッドをオーバーライドしてコンテキスト更新イベントを受け取りたい場合は自由にできますが、自身のコードを実行する前に必ず [super update] を呼び出し、コンテキストが更新を正しく処理していることを保証する必要があります。また、前述のように、クライアントは -update メソッドを慎重に使用する必要があります。なぜなら、このメソッドはウインドウのドラッグ時、ライブリサイズ中に定期的に、およびディスプレイ設定の変更のたびに呼び出されるため、ここでの処理が重くなると、ウインドウのドラッグやライブリサイズのときにアプリケーションが反応していないように見えてしまうためです。


Cocoa のカスタム OpenGL ビュー

NSOpenGLView をサブクラス化していないアプリケーションは、NSOpenGLContext を直接使用し、NSOpenGLContext の -update メソッドを通じてアプリケーションレベルで更新を処理する必要があります。リスト 1 のコードは、カスタム OpenGL ビュークラスの一部です。このクラスは、コンテキスト更新のベースとなるコールバックを提供する NSViewGlobalFrameDidChangeNotification にオブザーバを追加します。この通知の結果、NSOpenGLView が受け取るのと同じ更新、つまり表示のリサイズ、移動、座標オフセット、ディスプレイ設定変更が行われます。このクラスと NSOpenGLView が受け取るイベントには、若干の違いがあります。-update メソッドは必要なすべての場合(リサイズなど)に呼び出されますが、NSView はオーバーライドする特定のリシェイプメソッドをエクスポートしないため、-reshape メソッドはサイズ変更時には呼び出されません。クライアントは -drawRect メソッドで直接リシェイプを処理し、実際にコンテンツを描画する前に、表示範囲で変更を探すことをお勧めします。このアプローチは、NSOpenGLView の -reshape メソッドを使うときと比べ、パフォーマンスを特に改善したり低下させたりするものではありませんが、同じ結果が得られます。NSView のサイズ変更ロジックにフックすることはできますが、複雑であり、カスタムの OpenGL ビュークラスを構築している OpenGL アプリケーションでは通常はその手間をかける意味はありません。

#import <Cocoa/Cocoa.h>
#import <OpenGL/OpenGL.h>
#import <OpenGL/gl.h>

// コンテキスト更新の処理方法を示す、
// カスタム OpenGL ビュークラスの部分的な実装

@class NSOpenGLContext, NSOpenGLPixelFormat;

@interface CustomOpenGLView : NSView
{
  @private
  NSOpenGLContext*   _openGLContext;
  NSOpenGLPixelFormat* _pixelFormat;
}

- (id)initWithFrame:(NSRect)frameRect 
        pixelFormat:(NSOpenGLPixelFormat*)format;

// リシェイプはサポートされていないため、drawRect で範囲を更新
- (void)update;   // 移動、リサイズ、ディスプレイ変更

@end

// ---------------------------------

@implementation CustomOpenGLView

- (id)initWithFrame:(NSRect)frameRect 
        pixelFormat:(NSOpenGLPixelFormat*)format
{
  self = [super initWithFrame:frameRect];
  if (self != nil) {
    _pixelFormat   = [format retain];
  }
  [[NSNotificationCenter defaultCenter] addObserver:self 
                                 selector:@selector(_surfaceNeedsUpdate:) 
                                 name:NSViewGlobalFrameDidChangeNotification
                                 object:self];
  return self;
}

// ---------------------------------

- (void)dealloc
{ // コンテキストとピクセルフォーマットを削除
  [[NSNotificationCenter defaultCenter] removeObserver:self 
                                 name:NSViewGlobalFrameDidChangeNotification
                                 object:self];
  [self clearGLContext];
  [_pixelFormat release];
  
  [super dealloc];
}

// ---------------------------------

// NSView は特定のリシェイプメソッドをエクスポートしないため、リシェイプはなし

// ---------------------------------

- (void)update
{
  if ([_openGLContext view] == self) {
    [_openGLContext update];
  }
}

// ---------------------------------

- (void) _surfaceNeedsUpdate:(NSNotification*)notification
{
  [self update];
}

@end

リスト 1. NSView から派生したカスタムの OpenGL ビュークラス


GLUT

GLUT フレームワークを使っているアプリケーションは、GLUT がクライアントとのやり取りなしで適切なコンテキスト更新を実行するため、OpenGL コンテキストやウインドウを更新する必要はありません。このようなアプリケーションでは、単に、ユーザからの正式なリサイズを処理する GLUT リシェイプコールバックに応答することを保証する必要があるだけです。


Carbon の AGL

Carbon API を使って書かれた OpenGL クライアントの場合は、AGL フレームワークを使うことになります。ドローアブルのリサイズと移動のイベントを処理する最も単純な方法は、Carbon イベントを利用することです。ドローアブルのリサイズと移動を対象とするのであれば、ウインドウイベントの kEventWindowBoundsChangedkEventWindowZoomed を処理するだけで十分です。kEventWindowBoundsChanged イベントは、「ライブ」の ウインドウリサイズとウインドウドラッグの際に発生するため、必要なケースの大部分が処理されます。kEventWindowZoomed ウインドウイベントは、付加的なウインドウズームのケースを処理します。これらの Carbon イベントの詳細については、Carbon イベントのドキュメント と、「CarbonEvents.h」フレームワークヘッダを参照してください。リスト 2 のコードは、これらのイベント(とその他)を処理する単純なウインドウイベントハンドラを示しています。このサンプルでは、ウインドウイベントハンドラは、リスト 3 に示す resizeGL ルーチンを呼び出します。

#include <Carbon/Carbon.h>
// 注:サポートルーチンの handleWindowUpdate()、disposeGL()、
//       buildGL() は読者の方への課題として残しておく
//       resizeGL() はリスト 3 に示す

static pascal OSStatus windowEvtHndlr (EventHandlerCallRef myHandler, 
                                       EventRef event, 
                                       void* userData)
{
  WindowRef     window;
  AGLContext    aglContext = (AGLContext) userData; // コンテキストのストレージ
  Rect          rectPort = {0,0,0,0};
  OSStatus      result = eventNotHandledErr;
  UInt32        class = GetEventClass (event);
  UInt32        kind = GetEventKind (event);

  GetEventParameter(event, kEventParamDirectObject, typeWindowRef, 
                    NULL, sizeof(WindowRef), NULL, &window);
  if (window) {
    GetWindowPortBounds (window, &rectPort);
  }
  switch (class) {
    // 他の種類のイベントをここで処理
    case kEventClassWindow:
      switch (kind) {
        case kEventWindowActivated: // クリック起動、最初のフラッシュを
          // 防ぐために、最初は意図的に通過
        case kEventWindowDrawContent:
          // ウインドウ更新関数を呼び出す、たとえば...
          handleWindowUpdate(window);
          break;
        case kEventWindowClose: // ウインドウが閉じられている(クローズボックス)
          HideWindow (window);
          // OpenGL の破棄関数を呼び出す、たとえば...
          disposeGL (window);
          break;
        case kEventWindowShown: // 最初の表示(最小化の解除以外)
          // OpenGL のセットアップ関数を呼び出す、たとえば...
          buildGL (window);
          if (window == FrontWindow ())
            SetUserFocusWindow (window);
          InvalWindowRect (window, &rectPort);
          break;
        case kEventWindowBoundsChanged: // リサイズと移動(ドラッグ)
          resizeGL (window, aglContext);
          // ウインドウ更新関数を呼び出す、たとえば...
          handleWindowUpdate(window); // ライブリサイズのために強制的に更新
          // 注意:ウインドウの再描画が遅いとドラッグのパフォーマンスに影響する
          break;
        case kEventWindowZoomed: // ユーザがズームボタンをクリック
          resizeGL (window, aglContext);
          break;
      }
      break;
  }
  return result;
}

リスト 2. Carbon ウインドウイベントハンドラ

コンテキスト更新を処理する実際のコードをリスト 3 に示します。その最も単純な形式で、このコードは、対象のコンテキストが aglSetCurrentContext で現在のコンテキストになることを保証し、そのコンテキストに対して aglUpdateContext を呼び出します。前述のように、アプリケーションは glViewport は呼び出し、ドローアブルのサイズをコードリストに示すように現在のウインドウサイズ、または意味のある他の値に更新することもできます。さらに、クライアントは、ウインドウのサイズとその相対的なジオメトリが変更されてから描画ルーチンを使ってまだ更新していない場合に、この機会を利用してその投影行列を更新できます。

#include <Carbon/Carbon.h>
#include <AGL/agl.h>
#include <OpenGL/OpenGL.h>

// GL のリサイズを処理
// - コンテキストの更新が必要であり、ウインドウサイズが変更された場合、
//   ウインドウサイズを更新して、ビューポートをリセット

void resizeGL (WindowRef window, AGLContext aglContext)
{
  Rect rectPort;

  aglSetCurrentContext (aglContext);
  aglUpdateContext (aglContext);

  GetWindowPortBounds (window, &rectPort);
  glViewport (0, 0, rectPort.right - rectPort.left, 
                    rectPort.bottom - rectPort.top);
  // 必要に応じて、ここで投影行列を更新
}

リスト 3. Carbon コンテキスト更新ハンドラ

Carbon ディスプレイ設定変更の処理は若干複雑ですが、非常に合理的です。ディスプレイ設定の変更は、Display Manager コールバック関数を使って検出できます。この API は Carbon フレームワークでも利用可能で、そのプロトタイプが Displays.h ヘッダファイルの中にあります。デベロッパは、DMExtendedNotificationProcPtr コールバック API に準拠したコールバック関数を用意する必要があります。次に、NewDMExtendedNotificationUPP を通じてこの関数への UPP (Universal Procedure Pointer) を作成し、この UPP を Display Manager の DMRegisterExtendedNotifyProc に登録します。クライアントが複数のコンテキストまたはウインドウを使っている場合は、リスト 4 に示すように、ウインドウ、またはコンテキストをユーザデータに追加するのが役に立つことがあります。コールバック関数自体は単純なもので、上記のリスト 3 に示すようにコンテキスト更新ルーチンの呼び出しからなり、フルウインドウのグラフィックスポート矩形を無効にして更新イベントを強制します。コールバックに送られるメッセージタイプとして kDMNotifyEvent を探すことによって、イベントが実際に「通知」イベントであることを必ず確認する必要があります。通知イベント以外にも、コンテキスト更新の処理でクライアントが対処する必要のない Display Manager イベントがあります。リスト 4 には、コールバック、UPP の作成と登録、そして最後にアプリケーションが Display Manager の通知を必要としなくなった場合の、DisposeDMExtendedNotificationUPP による UPP の破棄を示します。

#include <Carbon/Carbon.h>
#include <AGL/agl.h>

// コンテキストは userData を通じて渡される
void handleWindowDMEvent (void *userData, short msg, void *notifyData)
{
  AGLContext aglContext = (AGLContext) userData; // コンテキストのストレージ
  if (kDMNotifyEvent == msg) { // 変更通知のみを送る
    resizeGL (window, agContext); // コンテキストの更新とリサイズの処理
    GetWindowPortBounds (window, &rectPort);
    InvalWindowRect (window, &rectPort); // 強制的に再描画
  }
}

// ---------------------------------

void setupDMNotify (WindowRef window)
{
  // ディスプレイ設定が変更されたときに必ず通知されるようにする
  gWindowEDMUPP = NewDMExtendedNotificationUPP (handleWindowDMEvent);
  DMRegisterExtendedNotifyProc (gWindowEDMUPP, (void *)window, NULL, &psn);
}

// ---------------------------------

OSStatus disposeDM Notify (WindowRef window)
{
  if (gWindowEDMUPP) { // DM 通知の UPP を破棄
    DisposeDMExtendedNotificationUPP (gWindowEDMUPP);
    gWindowEDMUPP = NULL;
  }
}

リスト 4. Display Manager 通知ハンドラ


フルスクリーンの AGL と CGL

フルスクリーンの AGL と CGL を使用するアプリケーションの場合は、作業はもう少し単純になります。ドローアブルの位置が固定され、サイズはディスプレイ設定に直接結び付いており、この設定がアプリケーションの制御下にあるため、フルスクリーンアプリケーションは実際に設定を変更した場合に、単に更新を実行する必要があります。フルスクリーンクライアントでは、コンテキスト更新ルーチンを呼び出す代わりに、フルスクリーンをセットする呼び出しを再発行するだけです。リスト 5 と 6 は、それぞれフルスクリーンコンテキストをリセットする AGL ルーチンと CGL ルーチンの例です。AGL の場合は、aglSetFullScreen 関数がスクリーンキャプチャとディスプレイリサイズを処理するため、resizeGL を呼び出す前に、有効なフルスクリーンピクセルフォーマットとコンテキストが作成されていることを保証する必要があるだけです。CGL の場合は、CGCaptureAllDisplaysCGDisplayBestModeForParametersAndRefreshRate(または、関連する CGDirectDisplay 関数)、および CGDisplaySwitchToMode を使って、要求されたディスプレイ設定をセットし、次にディスプレイのピクセルフォーマットを設定し、resizeGL を呼び出します。リスト 7 に、CGL で使用できるように OpenGL を設定する例を示します。

注:
AGL_FS_CAPTURE_SINGLE を設定せずに aglSetFullScreen を使うか、または CGCaptureAllDisplays を使ってすべてのディスプレイをキャプチャすると、ディスプレイ設定が固定されており、解放するまで変わらないため、アプリケーションは Display Manager の通知を受け取りません。クライアントが、すべてのディスプレイをキャプチャしていない場合、キャプチャされていないディスプレイのディスプレイ設定の変更は依然として受け取ります。このような通知は、現在はディスプレイには使用されておらず、フルスクリーンの OpenGL の処理の対象ではないため、通常、フルスクリーンアプリケーションは、このようなディスプレイ通知を処理する必要がありません。


#include <Carbon/Carbon.h>
#include <AGL/agl.h>
#include <OpenGL/gl.h>

// スクリーンのリサイズと更新に必要なコンテキストを処理。
// フルスクリーン、使用する単一の GDevice、設定するピクセル深さ
// を指定するピクセルフォーマットでコンテキストが作成されて
// いることを前提とする。

void resizeGL (AGLContext aglContext, GLSizei height, GLSizei width)
{
  GLint displayCaps [3];

  if (!aglContext) // 有効なコンテキストかどうかをチェック
    return;

  // コンテキストが確実に更新されるように、ドローアブルを再度アタッチ
  aglSetCurrentContext (aglContext);
  aglSetFullScreen (aglContext, width, height, 0, 0);
  // 注意:深さはピクセルフォーマットで設定、周波数 0 はすべてにマッチ
  // 0 ディスプレイはピクセルフォーマットで選択された単一のディスプレイを選択
  
  aglGetInteger (aglContext, AGL_FULLSCREEN, displayCaps); // サイズを取得
  glViewport (0, 0, displayCaps[0], displayCaps[1]);
  // 必要に応じて、ここで投影行列を更新
}

リスト 5. フルスクリーン AGL 更新の処理


#include <Carbon/Carbon.h>
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl.h>

// コンテキストの更新に必要な再アタッチを処理。
// フルスクリーン、使用する単一のディスプレイ、設定するピクセル深さ
// を指定するピクセルフォーマットでコンテキストが作成されて
// いることを前提とする。
// さらに、スクリーンがキャプチャされ、要求されたサイズに設定されて
// いることを前提とする。呼び出しルーチンは実際のディスプレイサイズ
// の設定を実際に処理するため、ここではビューポートは設定されない。


void resizeGL (CGLContextObj cglContext)
{
  if (!cglContext) // 有効なコンテキストかどうかチェック
    return;

  // コンテキストが確実に更新されるように、ドローアブルを再度アタッチ
  CGLSetCurrentContext (cglContext);
  CGLSetFullScreen (cglContext);
}

リスト 6. フルスクリーン CGL 更新の処理


#include <Carbon/Carbon.h>
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl.h>

// フルスクリーンのピクセルフォーマットの作成、ディスプレイキャプチャ、
// およびリサイズの例を示すコード。さらに関連する
// 解除コードも示す。

// 解除に使用するグローバル変数
CGDirectDisplayID gDisplay = 0;
CFDictionaryRef gOldDisplayMode = NULL;
GLboolean gOldDisplayModeValid = GL_FALSE;

CGLContextObj buildFullScreenGL (size_t width, size_t height, 
                                 size_t depth, CGRefreshRate refresh)
{
  CGLContextObj cglContext = 0;
  CGLPixelFormatAttribute attribs[] = {kCGLPFADisplayMask, 0, 
                                       kCGLPFAFullScreen, 
                                       kCGLPFADoubleBuffer, 
                                       kCGLPFADepthSize, 16, NULL};
  CGLPixelFormatObj pixelFormat = NULL;
  long numPixelFormats = 0;
  CFDictionaryRef refDisplayMode = 0;
  CGRect displayRect;

  // ディスプレイモードを設定
  gDisplay = CGMainDisplayID (); // メインのディスプレイを使用
  refDisplayMode = CGDisplayBestModeForParametersAndRefreshRate 
                        (gDisplay, depth, width, height, refresh, NULL);
  if (refDisplayMode) {
    gOldDisplayMode = CGDisplayCurrentMode (gDisplay);
    gOldDisplayModeValid = GL_TRUE;
    CGCaptureAllDisplays ();
    CGDisplaySwitchToMode (gDisplay, refDisplayMode);
  } // それ以外の現在のモードを使用
  
  // コンテキストを構築
  attribs[1] = CGDisplayIDToOpenGLDisplayMask (gDisplay); // PF ディスプレイを設定
  CGLChoosePixelFormat (attribs, &pixelFormat, &numPixelFormats);
  if (pixelFormat) {
    CGLCreateContext (pixelFormat, NULL, &cglContext);
    CGLDestroyPixelFormat (pixelFormat);
  }
  if (cglContext) {
    resizeGL (cglContext);
    displayRect = CGDisplayBounds (gDisplay);
    glViewport (0, 0, displayRect.size.width, displayRect.size.height);
    // 必要に応じて、投影行列を更新できる

    // ここで OpenGL の状態をセットアップ
  }
  return cglContext;
}

// ---------------------------------

void disposeGL (CGLContextObj cglContext)
{
  // コンテキストをダンプ
  CGLSetCurrentContext (NULL);
  CGLClearDrawable (cglContext);
  if (cglContext)
    CGLDestroyContext (cglContext);   

  // 適切な解像度に切り替え
  if (gOldDisplayModeValid)
    CGDisplaySwitchToMode(gDisplay, gOldDisplayMode);
  gOldDisplayModeValid = GL_FALSE;
  CGReleaseAllDisplays ();
}

リスト 7. フルスクリーン CGL ディスプレイの設定と解除の例


注:
CGL は CGDirectDisplayID の代わりに、ピクセルフォーマット属性の中で CGOpenGLDisplayMask を使って、フルスクリーンコンテキストに使うディスプレイを指定します。この属性は kCGLPFADisplayMask 指定子と、そのあとに実際のディスプレイマスクを続けることによって設定します。このマスクは CGDisplayIDToOpenGLDisplayMask 関数を使って、要求されたディスプレイの CGDirectDisplayID を渡すことによって見つけられます。


要約すると、OpenGL API のクライアントは、レンダラとジオメトリの変更に合わせて OpenGL コンテキストが更新されることを保証する必要があります。これはドローアブルのジオメトリを変更するイベント(サーフェスの原点、幅、高さ、ピクセルの深さ、ドローアブルの位置、ディスプレイ設定など)に応答して、システム API または クライアント自身によって処理されます。Carbon API の場合は、ウインドウ化されたクライアントでは aglUpdateContext を呼び出す必要があります。フルスクリーンアプリケーションでは、aglSetFullScreen または CGLSetFullScreen を使用して、フルスクリーンのドローアブルを再構築する必要があります。NSOpenGLView サブクラスを使用している Cocoa クライアントと GLUT アプリケーションでは、更新はシステム API によって処理します。最後に、カスタムの OpenGL ビュークラスを使用している Cocoa アプリケーションでは、ジオメトリの変更が通知されたときに、NSOpenGLContext の -update メソッドを呼び出す必要があります。上記のメソッドを使用するアプリケーションは、レンダラが変更されたり、ユーザがドローアブルを変更したりしたときに問題なく動作して、良好なユーザ体験を実現する必要があります。


[2003 年 6 月 19 日]