日別アーカイブ: 2013年9月5日

OpenGL と sRGB

一般的な画像はモニタに合わせてガンマ補正した非線形なデータになっています。
そのまま演算に用いると正しい結果が得られないため、事前にリニア値への
変換が必要になります。
同様に出力時は元のモニタ用の色空間に戻します。

・入力時 sRGB → Linear 変換
・出力時 Linear → sRGB 変換

OpenGL (2.1/3.0) や OpenGL ES 3.0 以降は標準である sRGB 色空間に対応しており、
リニア値との相互変換ができるようになっています。
変換が行われるのは上記のようにテクスチャの読み込みと、フレームバッファへの
書き込みのタイミングです。

●入力時

intenal format に sRGB 形式であることを指定できます。
テクスチャフェッチで同時に Linear に展開されます。
圧縮フォーマットも同様です。

// Linear
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data );
glCompressedTexImage2D( GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB8_ETC2, w, h, 0, size, data );
glCompressedTexImage2D( GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB_S3TC_DXT1_EXT, w, h, 0, size, data );

// sRGB
glTexImage2D( GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data );
glCompressedTexImage2D( GL_TEXTURE_2D, 0, GL_COMPRESSED_SRGB8_ETC2, w, h, 0, size, data );
glCompressedTexImage2D( GL_TEXTURE_2D, 0, GL_COMPRESSED_SRGB_S3TC_DXT1_EXT, w, h, 0, size, data );

変換は RGB のみで Alpha は含まれません。
OpenGL 4.x RADEON/GeForce/Intel HD 4000、OpenGL ES 3.0 Mali-T604 で
動作を確認しています。

OpenGL ES 2.0 で用いられていた GPU 固有の圧縮フォーマットには sRGB 形式が
定義されていないものがあります。
現在わかっている情報からまとめると下記の通り。

          OpenGL ES 3.0 での sRGB
----------------------------------------
ETC1         あり(ETC2)
ETC2         あり
PVRTC        無し         (PowerVR のみ)
PVRTC2       無し         (PowerVR のみ)
ATITC        無し         (Adreno のみ)
ASTC         あり
S3TC(DXT1)   あり

ETC1 に対応する sRGB フォーマットがありませんが、ETC2 が上位互換なので
GL_COMPRESSED_SRGB8_ETC2 で読み込むことが出来ます。
OpenGL ES 3.0 GPU は ETC2/EAC に対応しているので、SRGB 形式が必要なら
ETC2 を選択することになるかと思います。

フィルタリングに関与できないので厳密ではありませんが、
シェーダーで Linear に変換することも可能です。
ガンマ補正はデータ圧縮の一種といえるので、sRGB 8bit を展開すると
リニア値は 8bit 以上の精度が必要となります。

●出力時

フレームバッファが sRGB 形式で作られている場合、書き込み時に変換が行われます。
自分で作る場合は簡単で、Texture 同様に internal format に sRGB 形式を指定します。

// Renderbuffer
glGenRenderbuffers( 1, &ColorBuffer );
glBindRenderbuffer( GL_RENDERBUFFER, ColorBuffer );
glRenderbufferStorage( GL_RENDERBUFFER, GL_SRGB8_ALPHA8, w, h );
glBindRenderbuffer( GL_RENDERBUFFER, 0 );

glBindFramebuffer( GL_DRAW_FRAMEBUFFER, FrameBuffer );
glFramebufferRenderbuffer( GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ColorBuffer );
glBindFramebuffer( GL_DRAW_FRAMEBUFFER, 0 );

// Texture
glGenTextures( 1, &ColorTexture );
glBindTexture( GL_TEXTURE_2D, ColorTexture );
glTexImage2D( GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL );
glBindTexture( GL_TEXTURE_2D, 0 );

glBindFramebuffer( GL_DRAW_FRAMEBUFFER, FrameBuffer );
glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ColorTexture, 0 );
glBindFramebuffer( GL_DRAW_FRAMEBUFFER, 0 );

OS が用意する Default Framebuffer の場合は wgl や egl を用いて
SRGB 対応のフォーマットを選んでおく必要があります。

ただし Windows 7/8 + RADEON/GeForce では、特に何もしなくても SRGB 形式となっており
glEnable( GL_FRAMEBUFFER_SRGB ) だけで意図した結果が得られるようです。
逆に Intel HD 4000 (9.18.10.3165) では、下記の方法で SRGB バッファを選択しても
Default Framebuffer での変換がうまく出来ませんでした。

wgl を使った選択手段は下記のとおり。

1. wgl extension の wglChoosePixelFormatARB() を取得
2. 必要なパラメータ(attribute) を指定して wglChoosePixelFormatARB() で候補を選ぶ
3. 得られた Config 値を SetPixelFormat() に渡す

static const int PixelFormat_Attr[]= {
    WGL_DRAW_TO_WINDOW_ARB,  TRUE,
    WGL_SUPPORT_OPENGL_ARB,  TRUE,
    WGL_DOUBLE_BUFFER_ARB,   TRUE,
    WGL_PIXEL_TYPE_ARB,      WGL_TYPE_RGBA_ARB,
    WGL_COLOR_BITS_ARB,      32,
    WGL_RED_BITS_ARB,        8,
    WGL_GREEN_BITS_ARB,      8,
    WGL_BLUE_BITS_ARB,       8,
    WGL_ALPHA_BITS_ARB,      8,
    WGL_DEPTH_BITS_ARB,      24,
    WGL_STENCIL_BITS_ARB,    8,
    WGL_ACCELERATION_ARB,    WGL_FULL_ACCELERATION_ARB,
    WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB,   TRUE,
    0
};

int ChooseConfig()
{
    int  pixelformat= 0;
    if( !p_wglChoosePixelFormatARB ){ // extension の確認
        return	ChoosePixelFormat( hDC, &PixelFormat_Default );
    }
    const unsigned int	FORMAT_ARRAY_SIZE= 16;
    int	FormatArray[FORMAT_ARRAY_SIZE];
    unsigned int    array_size= 0;
    if( !wglChoosePixelFormatARB( hDC, PixelFormat_Attr, NULL, FORMAT_ARRAY_SIZE, FormatArray, &array_size ) ){
        return  0;
    }
    return  FormatArray[0];
}

void SetPixelFormat()
{
   int pixelformat= ChooseConfig();
   if( !pixelformat ){
      // error
   }
   SetPixelFormat( hDC, pixelformat, NULL );
}

Intel HD 4000 でも自分で Framebuffer を用意した場合は
glEnable( GL_FRAMEBUFFER_SRGB ) の設定が結果に影響を与えていることを確認できます。
ただし OpenGL 4.0 では結果をそのまま出力する手段が見つかりませんでした。

glBlitFramebuffer() や Texture としての読み込みは、ソースが sRGB の場合に
リニア変換されてしまうので、元が Linear か sRGB か区別出来ないからです。
よって Default Framebuffer に転送する場合にシェーダーでもう一度再エンコードが
必要となります。
OpenGL 4.2 以降ならメモリイメージとして転送できるので、もっと良い方法が
あるかもしれません。

OpenGL ES 3.0 (Mali-T604) では GL_FRAMEBUFFER_SRGB による切り替えが
できないので、Framebuffer に対して正しく変換されているかどうか確認できませんでした。
SRGB はブレンドの結果に影響を与えるので、ブレンド結果を比べれば
機能していること確認できるのではないかと思っています。

入力側は容易でしたが、出力側は必ずしも統一した挙動になってくれないようです。
Framebuffer が HDR の場合リニアのまま格納できるので、互換性を考えると
最終的にシェーダーで変換するのが一番なのかもしれません。

// GLSL
const float  SCALE_0= 1.0/12.92;
const float  SCALE_1= 1.0/1.055;
const float  OFFSET_1= 0.055 * SCALE_1;

float SRGBToLinear_F( float color )
{
    if( color <= 0.04045 ){
        return  color * SCALE_0;
    }
    return  pow( color * SCALE_1 + OFFSET_1, 2.4 );
}
float LinearToSRGB_F( float color )
{
    color= clamp( color, 0.0, 1.0 );
    if( color < 0.0031308 ){
        return  color * 12.92;
    }
    return  1.055 * pow( color, 0.41666 ) - 0.055;
}
vec3 LinearToSRGB_V( vec3 color )
{
    return  vec3(
        LinearToSRGB_F( color.x ),
        LinearToSRGB_F( color.y ),
        LinearToSRGB_F( color.z ) );
}
vec3 SRGBToLinear_V( vec3 color )
{
    return  vec3(
        SRGBToLinear_F( color.x ),
        SRGBToLinear_F( color.y ),
        SRGBToLinear_F( color.z ) );
}

関連エントリ
OpenGL 3.x/4.x のシェーダー GLSL とメモリアクセス命令
OpenGL 4.x Program Pipeline Object (Separate Shader Object)
OpenGL 4.2/4.3 Shader Resource と Buffer API
OpenGL ES 3.0/OpenGL 4.x Uniform Block
OpenGL の各バージョンと GLSL の互換性
OpenGL のエラー判定と OpenGL 4.3 Debug Output
OpenGL ES 3.0/OpenGL 4.4 Texture Object と Sampler Object
OpenGL ES 3.0 と Vertex Array Object