OpenGL 3.2 の GeometryShader

OpenGL 3.2 では core 機能に Geometry Shader が含まれています。
試してみました。

OpenGL Registry

GeometryShader はポリゴンの面単位で実行可能なシェーダープログラムです。
面単位なので、プリミティブタイプが Triangle List (GL_TRIANGLES) なら一度に
3頂点を入力として受け取ります。主な用途は下記の通りです。

・頂点演算
・面単位のマテリアル演算
・形状の操作
・出力先の変更
・StreamOutput (Transform Feedback)

わかりやすいところでは面法線の生成など面単位のマテリアル設定が考えられます。
ユニークなのは入力と関係なく出力プリミティブを組み立てられることです。
出力頂点数は任意なので、エッジだけ Line として書き出したり、面を分割したり、
面を消すことも出来ます。

例えば入力を PointList (GL_POINTS) で 1頂点だけ受け取り、Triangle で
2 プリミティブ出力すればポイントスプライトの代わりになるわけです。

出力先の変更は Direct3D だと RenderTarget Array, OpenGL では
Layered Rendering と呼ばれているようです。
Geometry Shader で書き込むフレームバッファの選択が可能で、Cube Map への
レンダリングも一回で出来る仕様になっています。

入力可能なプリミティブタイプも Direct3D 10 同様に増えています。
下記は増えた分のみ。使えるのは GeometryShader が有効な場合だけに限られます。

GL_LINES_ADJACENCY
GL_LINE_STRIP_ADJACENCY
GL_TRIANGLES_ADJACENCY
GL_TRIANGLE_STRIP_ADJACENCY

あまり馴染みがない名称ですが、極端な言い方をすれば 4,6 頂点のプリミティブが
使えるようになったということです。
GL_TRIANGLES だと GeometryShader は 3 頂点単位で受け取りますが、
GL_TRIANGLES_ADJACENCY では一度に 6頂点受け取れることになります。
GL_LINES_ADJACENCY の 4頂点は、QUAD プリミティブの代わりとしても使えます。

ちなみに Direct3D 11 の ShaderModel 5.0 はさらに増えていて、テセレータ用に
32頂点まで任意の数の頂点を入力できるようになっています。

今回使用したビデオカードは GeForce GTX280。ステートを調べると下記の通りです。
頂点の生成が最大 1024 float なのも Direct3D 10 の ShaderModel 4.0 と同じです。

GL_MAX_GEOMETRY_OUTPUT_VERTICES = 1024
GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS = 32
GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS = 1024

● GeometryShader のコンパイル

Shader の読み込みとコンパイル手順はこれまでの VertexShader, FragmentShader
と同じです。もともとシェーダーしか使えない OpenGL ES 2.0 を使っていたため、
ちょうど GeometryShader の部分が追加された形になります。

GLuint vshader= glCreateShader( GL_VERTEX_SHADER );
glShaderSource( vshader, 1, vshader_source_text, NULL );
glCompileShader( vshader );

GLuint gshader= glCreateShader( GL_GEOMETRY_SHADER );
glShaderSource( gshader, 1, gshader_source_text, NULL );
glCompileShader( gshader );

GLuint pshader= glCreateShader( GL_FRAGMENT_SHADER );
glShaderSource( pshader, 1, pshader_source_text, NULL );
glCompileShader( pshader );

エラー判定は省いてあります。

次はコンパイルしたシェーダーをリンクして Program の作成です。
これまで VertexShader, FragmentShader だけ attach していたところに
GeometryShader が加わります。

GLuint shaderProgram= glCreateProgram();
glAttachShader( shaderProgram, vshader );
glAttachShader( shaderProgram, gshader );
glAttachShader( shaderProgram, pshader );
glLinkProgram( shaderProgram );

●ドライバの補足

GLSL の GeometryShader の書き方がわからなかったため、ドキュメント
OpenGL Shadeing Language 1.50.09 と Direct3D の知識を頼りに進めました。

最初はコンパイルすらうまく通らず、EmitVertex を用いると
「#extension GL_EXT_geometry_shader4 : enable」
を宣言しなさいとエラーが出てしまいます。

「#version 150」で GLSL 1.5 を指定すると未対応のバージョンだといわれます。
140 なら通ります。ドライバを調べてみると、最新版 190.62 は OpenGL 3.1 しか
対応していませんでした。
ドライバが OpenGL 3.2 / GLSL 1.5 に対応していなかったことが原因です。

NVIDIA OpenGL 3.0/3.1/3.2 Support for Windows, Linux, FreeBSD, and Solaris

上のページにある、OpenGL 3.2 対応ドライバ 190.57 を入れたら、#version 150 指定
による GLSL 1.5 も、GeometryShader 専用命令も通るようになりました。
「#version 150」とだけ書いておけばよく、extension 指定は不要です。

● GLSL v1.50

「#version 150」を指定したら VertexShader でもエラーが出るようになりました。
attribute, varying 宣言は古いので、in, out を使えと言われます。

もともと attribute は頂点からの入力、varying はラスタライザの補間パラメータを
意味しています。出力先が GeometryShader かもしれないし、シェーダーパイプラインが
多層化&汎用化されたので名称が変わったものと思われます。

OpenGL ES 2.0 との互換性も残したいのでとりあえず define でごまかしました。
attribute を in に、varying を out に置き換えます。

// sysdef2.vsh (Vertex Shader)

#define attribute   in
#define varying     out

#version 150

uniform vec4	World[3];
uniform mat4	PView;

attribute vec3	POSITION;
attribute vec3	NORMAL;
attribute vec2	TEXCOORD;

varying vec3	onormal;
varying vec2	otexcoord;

void main()
{
    gl_Position= vec4( POSITION.xyz, 1 ) * PView;
    vec3    nvec;
    nvec.x= dot( NORMAL.xyz, World[0].xyz );
    nvec.y= dot( NORMAL.xyz, World[1].xyz );
    nvec.z= dot( NORMAL.xyz, World[2].xyz );
    onormal= nvec;
    otexcoord= TEXCOORD;
}

Matrix 周りが少々不自然なのは、OpenGL ES 2.0 に mat4x3 が無かったことと頂点
アニメーションを削ったためです。もともと Skinning の処理が入っていました。

同じように Fragment Shader も書き換えます。

// sysdef2.fsh (Fragment Shader)

#define varying     in

#version 150

varying	vec3    onormal;
varying	vec2    otexcoord;
uniform sampler2D   ColorMap;

void main()
{
    vec3  lightdir= vec3( 0.0, 0.0, -1.0 );
    float lv= clamp( dot( normalize( onormal ), lightdir ), 0.0, 1.0 ) + 0.5;
    vec4  color= vec4( 1.0, 1.0, 1.0, 1.0 ) * lv;
    vec4  tcol= texture2D( ColorMap, otexcoord );
    gl_FragColor= color * tcol;
}

FragmentShader の場合 VertexShader と逆で varying が in になります。

GeometryShader は最初から in/out で記述します。

// sysdef2.gsh (Geometry Shader)

#version 150

layout(triangles) in;
layout(triangle_strip, max_vertices= 3) out;

in Inputs {
    vec3    onormal;
    vec2    otexcoord;
} gin[3];

out vec3    onormal;
out vec2    otexcoord;

void main()
{
    gl_Position= gl_in[0].gl_Position;
    onormal= gin[0].onormal;
    otexcoord= gin[0].otexcoord;
    EmitVertex();

    gl_Position= gl_in[1].gl_Position;
    onormal= gin[1].onormal;	// (1)
    otexcoord= gin[1].otexcoord;
    EmitVertex();

    gl_Position= gl_in[2].gl_Position;
    onormal= gin[2].onormal;	// (1)
    otexcoord= gin[2].otexcoord;
    EmitVertex();

    EndPrimitive();
}

プリミティブのタイプは layout() で宣言しています。
入力は block 宣言を使っており、Triangle なので 3頂点分のデータを受け取ります。
VertexShader が書き込んだシステム変数 gl_Position の値は、こちらも builtin
の gl_in[] という配列で受け取れます。

出力はこれまで通り out (varying) 宣言した変数に書き込みます。
必要なパラメータを書き込んだ後に EmitVertex() 命令で頂点が確定します。
プリミティブの区切りは EndPrimitive() 命令です。

このジオメトリシェーダーは何もせずに、頂点の出力をそのままスルーしています。
上の (1) の行を削除すると面に同じ法線が適用されるため、フラットシェーディング
風の見た目になるはずです。

同じシェーダーを DirectX の HLSL で書くとこんな感じです。

// HLSL Geometry Shader

struct GS_INPUT {
    float4   Position  : POSITION;
    float3   Normal    : NORMAL;
    float2   Texcoord  : TEXCOORD;
};

struct GS_OUTPUT {
    float4   Position  : SV_POSITION;
    float3   Normal    : NORMAL;
    float2   Texcoord  : TEXCOORD;
};

[maxvertexcount(3)]
void GS_Main( triangle GS_INPUT In[3], inout TriangleStream gsstream )
{
    GS_OUTPUT   Out;

    Out.Position= In[0].Position;
    Out.Texcoord= In[0].Texcoord;
    Out.Normal  = In[0].Normal;
    gsstream.Append( Out );

    Out.Position= In[1].Position;
    Out.Texcoord= In[1].Texcoord;
    Out.Normal  = In[1].Normal;
    gsstream.Append( Out );

    Out.Position= In[2].Position;
    Out.Texcoord= In[2].Texcoord;
    Out.Normal  = In[2].Normal;
    gsstream.Append( Out );

    gsstream.RestartStrip();
}

書式が違うだけでほとんど同じです。
Direct3D でも、アセンブラ出力すると頂点の生成は emit 命令なのです。

関連エントリ
OpenGL のはじめ方
OpenGL ES 2.0 Emulator
OpenGL ES 2.0
OpenGLES2.0 DDS テクスチャを読み込む
OpenGLES2.0 Direct3D とのフォーマット変換
OpenGLES 2.0 頂点フォーマットの管理
OpenGLES2.0 の頂点
OpenGLES2.0 D3D座標系
OpenGLES2.0 シェーダー管理