日別アーカイブ: 2013年8月17日

OpenGL 4.x Program Pipeline Object (Separate Shader Object)

OpenGL のシェーダーは Direct3D と違い、API レベルでシンボル管理が行われます。

Direct3D の Native な API では、Vertex Shader, Pixel Shader,
Geometry Shader などの、各シェーダー毎に Object が分かれています。
ステートやバインド情報もそれぞれ別で、レンダリングのタイミングで
必要な情報を Direct3D の DeviceContext に割り当てます。
Constant (Unifrom) の管理も各シェーダー毎に別空間です。

例えば Camera 座標など、Vertex Shader, Pixel Shader 両方が
アクセセスするパラメータがあった場合は
PSSetConstantBuffers(), VSSetConstantBuffers()
両方に設定しなければなりません。
これはプログラマの仕事です。

OpenGL の場合 Vertex Shader, Fragment Shader, Geometry Shader など、
描画に必要なシェーダーを集めて最初に Program Object を作っておきます。
同時に Uniform や Block 等のシンボルの共有化が行われており、
どのシェーダーから参照されているのか情報が保持されます。

各シンボルの参照状態は OpenGL 4.3 以降は
glGetProgramResourceiv( GL_REFERENCED_BY_*_SHADER )
を使って調べることが出来ます。

プログラマはどのシェーダーで使われているのか意識することなく
単一のシンボルに対して設定すればよいだけです。
もちろん Vertex Shader, Fragment Shader 両方から参照されているなら
両方とも同じ値に更新されます。

OpenGL 4.1 から利用可能となった Program Pipeline Object は、
このような OpenGL Program Object の一括管理とは正反対のものです。
OpenGL 上で Direct3D のような、より低レベルだけど自由度の高い
シェーダー管理を可能にします。

GPU 周りの進化は非常に速いので、単一の仕組みだけでは様々な
要求に応えることが難しくなってきているのかもしれません。

●Shader の組み合わせ

レンダリングに使われるシェーダーのバリエーションはかなり膨大な量になります。
手で管理することは不可能なので、描画用のエンジン開発では
最初にシェーダー管理の仕組みを構築することになります。

ある程度の組み合わせに融通は利くので、用途に応じて
Pixel Shader (Fragment Shader) だけ変えたり、
部分的に使いまわしたりすることがありました。

OpenGL の場合 Shader Object レベルでは組み合わせを作ることが出来ます。
実際の利用時は Program Object が必要となるので、組み合わせの数だけ
Program Object が出来上がります。

Program Pipeline Object は、既存の OpenGL API の仕組みを利用したまま
動的に Shader を入れ替えられるようにしています。

 1. Shader 単独の Program Object を作る
 2. Pipeline Object にそれぞれ登録する
 3. 描画時に glUseProgram() の代わりに glBindProgramPipeline() を使う

//● 通常のシェーダーの場合

// Vertex Shader
GLuint vsh_obj= glCreateShader( GL_VERTEX_SHADER );
glShaderSource( vsh_obj, 1, &vsh_source_text );
glCompileShader( vsh_obj );

// Fragment Shader
GLuint fsh_obj= glCreateShader( GL_FRAGMENT_SHADER );
glShaderSource( fsh_obj, 1, &fsh_source_text );
glCompileShader( fsh_obj );

// Program Object を作成 (Link)
GLuint program_obj= glCreateProgram();
glAttachShader( program_obj, vsh_obj );
glAttachShader( program_obj, fsh_obj );
glLinkProgram( program_obj );

// 利用時
glUseProgram( program_obj );

Pipeline Object を利用する場合は下記の通り。

//● Pipeline Object を使う場合

// Vertex Shader
GLuint vsh_obj= glCreateShader( GL_VERTEX_SHADER );
glShaderSource( vsh_obj, 1, &vsh_source_text );
glCompileShader( vsh_obj );

// 単独の Vertex Shader Program に変換
GLuint vsh_program= glCreateProgram();
glProgramParameteri( vsh_program, GL_PROGRAM_SEPARABLE, GL_TURE );
glAttachShader( vsh_program, vsh_obj );
glLinkProgram( vsh_program );


// Fragment Shader
GLuint fsh_obj= glCreateShader( GL_FRAGMENT_SHADER );
glShaderSource( fsh_obj, 1, &fsh_source_text );
glCompileShader( fsh_obj );

// 単独の Fragment Shader Program に変換
GLuint fsh_program= glCreateProgram();
glProgramParameteri( fsh_program, GL_PROGRAM_SEPARABLE, GL_TURE );
glAttachShader( fsh_program, fsh_obj );
glLinkProgram( fsh_program );


// Pipeline Object
GLuint pipeline= 0;
glGenPipelineObjects( 1, &pipeline );
glGenProgramPipelines( 1, &pipeline );
glUseProgramStages( pipeline, GL_VERTEX_SHADER_BIT, vsh_program );
glUseProgramStages( pipeline, GL_FRAGMENT_SHADER_BIT, fsh_program );


// 利用時
glUseProgram( 0 );
glBindProgramPipeline( pipeline );

↑一見複雑ですが、Program Object を作るための便利な命令↓が増えています。

//● Pipeline Object を使う場合
GLuint vsh_program= glCreateShaderProgramv( GL_VERTEX_SHADER, 1, &vsh_source_text );
GLuint psh_program= glCreateShaderProgramv( GL_FRAGMENT_SHADER, 1, &fsh_source_text );

// Pipeline Object
GLuint pipeline= 0;
glGenPipelineObjects( 1, &pipeline );
glGenProgramPipelines( 1, &pipeline );
glUseProgramStages( pipeline, GL_VERTEX_SHADER_BIT, vsh_program );
glUseProgramStages( pipeline, GL_FRAGMENT_SHADER_BIT, fsh_program );


// 利用時
glUseProgram( 0 );
glBindProgramPipeline( pipeline );

↑簡単になりました。

●情報アクセス

Pipeline Object は複数の Program Object を取りまとめる器に過ぎず、
いつでも Program Object の追加と削除ができます。

Link のように大掛かりなデータ共有は行わず、入出力など最低限の
マッチングだけ行います。
入出力のマッチングはシンボル名または Location を元に行われるようです。

Program Object が持っていた情報もシェーダー個別に扱うことになります。
例えば Attribute の Location、Default Uniform Block などがそうです。

もともと Program Object のアクセス API は OpenGL の他の API と構造が
異なっており、多くのケースで Bind を必要としていませんでした。
引数として Program Object を取るものが多いのですが、唯一
glUniform() 系の API だけが glUseProgram() に依存しています。

方法は 3通り

1. Default Uniform Block を使わない

2. 直接 Program Object を引数にとる新しい API を使う

glUseProgram( vsh_program );
glUniform4fv( location, 1, vect );
 ↓
glProgramUniform4fv( vsh_program, location, 1, vect );

3. glActiveShaderProgram() を使う

glActiveShaderProgram() を使うと従来の Uniform 命令を使うことができます。

glUseProgram( program );
glUniform4fv( location, 1, vect );
 ↓
glUseProgram( 0 );
glBindProgramPipeline( pipeline );
glActiveShaderProgram( pipeline, vsh_program );
glUniform4fv( location, 1, vect );

●Bind Point

Pipeline Object は今までの OpenGL API にできるだけ影響を与えないように
作られています。
Bind Point は新たに追加されており、他のステートにも干渉しません。

例えば glUseProgram() が有効な場合、glUseProgram() が優先され
glBindProgramPipeline() は無視します。
Pipeline Object を有効にするにはこれまでの例で示したように
glUseProgram( 0 ) が必要です。

●実際のシェーダーと最適化の違い

Pipeline Object を使う場合、gl_Position も自分で定義しておきます。

#version 430

// Vertex Shader

uniform mat4	PView;
uniform mat4	World;
uniform vec4	UVOffset;

layout(location=0) in vec3 POSITION;
layout(location=1) in vec3 NORMAL;
layout(location=2) in vec2 TEXCOORD;

out vec3 onormal;
out vec2 otexcoord;

out gl_PerVertex {
    vec4 gl_Position;
};

void main()
{
    mat4  pview= World * PView;
    vec4  opos= vec4( POSITION.xyz, 1.0 ) * pview;
    opos.z= 2.0 * opos.z - opos.w;
    gl_Position= opos;
    onormal= NORMAL.xyz * mat3x3( World );
    otexcoord= TEXCOORD.xy + UVOffset.xy;
}
#version 430

// Fragment Shader

out vec4  out_FragColor;
uniform vec4   UVOffset;

in vec3	  onormal;
in vec2   otexcoord;
layout(binding=0) uniform sampler2D  ColorMap;

void main()
{
    float   diff= clamp( dot( vec3( 0, 0, -1 ), normalize( onormal.xyz ) ), 0.0, 1.0 );
    vec2    texcoord= otexcoord.xy - UVOffset.xy;
    vec4    color= texture( ColorMap, texcoord.xy );
    out_FragColor.xyz= color.xyz * diff;
    out_FragColor.w= 1.0;
}

UVOffset は Vertex と Fragment で同名です。
Pipeline Object を使った場合は異なる値に設定することが可能です。

上のコードのシンボル情報を調べてみます。
前回の API を使って情報を取り出したのが下記の結果です。
GeForce もほぼ同じでした。

// ●テクスチャあり Pipeline Object

// RADEON 4.3 : Vertex Shader
 Prg: 0011   Del=0 Link=1 Vali=1 Separable=1
Resource [Uniform]
  0: FLOAT_MAT4  Array=0 Loc=0 V_____  "PView"
  1: FLOAT_VEC4  Array=0 Loc=1 V_____  "UVOffset"
  2: FLOAT_MAT4  Array=0 Loc=2 V_____  "World"
Resource [Input]
  0: FLOAT_VEC3  Array=0       V_____  "NORMAL"
  1: FLOAT_VEC3  Array=0       V_____  "POSITION"
  2: FLOAT_VEC2  Array=0       V_____  "TEXCOORD"
Resource [Output]
  0: FLOAT_VEC4  Array=0       V_____  "gl_PerVertex.gl_Position"
  1: FLOAT_VEC3  Array=0       V_____  "onormal"
  2: FLOAT_VEC2  Array=0       V_____  "otexcoord"


// RADEON 4.3 : Fragment Shader
 Prg: 0012   Del=0 Link=1 Vali=1 Separable=1
Resource [Uniform]
  0: FLOAT_VEC4  Array=0 Loc=0 ____F_  "UVOffset"
  1: SAMPLER_2D  Array=0 Loc=1 ____F_  "ColorMap"
Resource [Input]
  0: FLOAT_VEC3  Array=0       ____F_  "onormal"
  1: FLOAT_VEC2  Array=0       ____F_  "otexcoord"
Resource [Output]
  0: FLOAT_VEC4  Array=0       ____F_  "out_FragColor"

次に Fragment Shader の texture 命令を削除してみます。

#version 430

// Fragment Shader

out vec4  out_FragColor;
uniform vec4   UVOffset;

in vec3	  onormal;
in vec2   otexcoord;

layout(binding=0) uniform sampler2D  ColorMap;

void main()
{
    float   diff= clamp( dot( vec3( 0, 0, -1 ), normalize( onormal.xyz ) ), 0.0, 1.0 );
    vec2    texcoord= otexcoord.xy - UVOffset.xy;
    //vec4    color= texture( ColorMap, texcoord.xy ); //** 削除
    vec4    color= vec4( 1.0, 1.0, 0.2, 1.0 );
    out_FragColor.xyz= color.xyz * diff;
    out_FragColor.w= 1.0;
}

↓VertexShader の方は変化していませんが、Fragment Shader の出力が
大きく減っていることがわかります。
texture()が無いので不要と判断され UVOffset も消えました。

// ●テクスチャ無し Pipeline Object

// GeForce 4.4 : Vertex Shader
 Prg: 0011   Del=0 Link=1 Vali=1 Separable=1
Resource [Uniform]
  0: FLOAT_MAT4  Array=0 Loc=0 V_____  "PView"
  1: FLOAT_VEC4  Array=0 Loc=1 V_____  "UVOffset"
  2: FLOAT_MAT4  Array=0 Loc=2 V_____  "World"
Resource [Input]
  0: FLOAT_VEC3  Array=0       V_____  "NORMAL"
  1: FLOAT_VEC3  Array=0       V_____  "POSITION"
  2: FLOAT_VEC2  Array=0       V_____  "TEXCOORD"
Resource [Output]
  0: FLOAT_VEC3  Array=0       V_____  "onormal"
  1: FLOAT_VEC2  Array=0       V_____  "otexcoord"
  2: FLOAT_VEC4  Array=0       V_____  "gl_Position"


// GeForce 4.4 : Fragment Shader
Resource [Input]
  0: FLOAT_VEC3  Array=0       ____F_  "onormal"
Resource [Output]
  0: FLOAT_VEC4  Array=0       ____F_  "out_FragColor"

↓RADEON はちょっとだけ違います。

// RADEON 4.3 : Fragment Shader
Resource [Input]
  0: FLOAT_VEC3  Array=0       ____F_  "onormal"
  1: FLOAT_VEC2  Array=0       ____F_  "otexcoord"
Resource [Output]
  0: FLOAT_VEC4  Array=0       ____F_  "out_FragColor"

↓同じシェーダーを Pipeline Object を使わずに作ってみます。
Separable (GL_PROGRAM_SEPARABLE) の値で区別できます。

// ●テクスチャあり Program Object

// GeForce 4.4 : Vertex + Fragment Shader
 Prg: 0014   Del=0 Link=1 Vali=1 Separable=0
  [0] 0012 VERTEX_SHADER           Del=1 Compile=1
  [1] 0013 FRAGMENT_SHADER         Del=1 Compile=1
Resource [Uniform]
  0: SAMPLER_2D  Array=0 Loc=0 ____F_  "ColorMap"
  1: FLOAT_MAT4  Array=0 Loc=1 V_____  "PView"
  2: FLOAT_VEC4  Array=0 Loc=2 V___F_  "UVOffset"
  3: FLOAT_MAT4  Array=0 Loc=3 V_____  "World"
Resource [Input]
  0: FLOAT_VEC3  Array=0       V_____  "NORMAL"
  1: FLOAT_VEC3  Array=0       V_____  "POSITION"
  2: FLOAT_VEC2  Array=0       V_____  "TEXCOORD"
Resource [Output]
  0: FLOAT_VEC4  Array=0       ____F_  "out_FragColor"

texture() を削除したものが下記の通り。

// ●テクスチャ無し Program Object

// GeForce 4.4 : Vertex + Fragment Shader
 Prg: 0014   Del=0 Link=1 Vali=1 Separable=0
  [0] 0012 VERTEX_SHADER           Del=1 Compile=1
  [1] 0013 FRAGMENT_SHADER         Del=1 Compile=1
Resource [Uniform]
  0: FLOAT_MAT4  Array=0 Loc=0 V_____  "PView"
  1: FLOAT_MAT4  Array=0 Loc=1 V_____  "World"
Resource [Input]
  0: FLOAT_VEC3  Array=0       V_____  "NORMAL"
  1: FLOAT_VEC3  Array=0       V_____  "POSITION"
Resource [Output]
  0: FLOAT_VEC4  Array=0       ____F_  "out_FragColor"

↑Fragment Shader の最適化が Vertex Shader まで派生していることがわかります。
Fragment Vertex 双方から不要と判断され UVOffset が消えました。
GeForce に至っては Attribute の TEXCOORD まで無くなっています。

// RADEON 4.3 : Vertex + Fragment Shader
 Prg: 0014   Del=0 Link=1 Vali=1 Separable=0
  [0] 0010 VERTEX_SHADER           Del=1 Compile=1
  [1] 0013 FRAGMENT_SHADER         Del=1 Compile=1
Resource [Uniform]
  0: FLOAT_MAT4  Array=0 Loc=0 V_____  "PView"
  1: FLOAT_MAT4  Array=0 Loc=1 V_____  "World"
Resource [Input]
  0: FLOAT_VEC3  Array=0       V_____  "NORMAL"
  1: FLOAT_VEC3  Array=0       V_____  "POSITION"
  2: FLOAT_VEC2  Array=0       V_____  "TEXCOORD"
Resource [Output]
  0: FLOAT_VEC4  Array=0       ____F_  "out_FragColor"

このように、今までどおり Link して単一の Program Object に変換する方が
最適化の面ではメリットがあることがわかります。

実パフォーマンスでどの程度影響が生じるかどうかは未確認です。
Direct3D で開発し、すでにシェーダー管理の仕組みができている場合は
互換性の意味でも便利な機能です。
自由な組み合わせが必要な場合とそうでない場合の使い分けが必要かもしれません。

2014/06/23訂正: glPipelineObjects を glProgramPipelines に修正しました。間違った記載をしており申し訳ありませんでした。
小川さんご指摘ありがとうございます。

関連エントリ
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