月別アーカイブ: 2009年9月

OpenGL GLSL のバージョン

GeometryShader 利用時に GLSL のバージョンの違いが問題になったのでまとめてみました。

OpenGL 1.3  ----           OpenGL ES 1.0  ----
OpenGL 1.5  *1             OpenGL ES 1.1  ----
OpenGL 2.0  GLSL 1.1       OpenGL ES 2.0  GLSL ES 1.0 (ESSL)
OpenGL 2.1  GLSL 1.2
OpenGL 3.0  GLSL 1.3
OpenGL 3.1  GLSL 1.4
OpenGL 3.2  GLSL 1.5

*1: Shader 対応。Extension で GLSL1.0 も利用可能。Core API になったのは OpenGL 2.0 から。

Wikipedia GLSL

OpenGL ES 2.0 の GLSL は、GLSL ES または ESSL と呼ばれています。

上の対応表では OpenGL 2.0 / GLSL 1.1 の列にありますが、GLSL ES 1.0 自体は
GLSL 1.2 の機能も含まれています。実際は OpenGL 2.0~2.1 の中間になります。

OpenGL 2.0 で GLSL が core に組み込まれました。
OpenGL 3.0 / GLSL 1.3 で大きな仕様変更があり、シェーダーも固定機能の排除が
進められています。attribute/varying が in/out に変更になったのもここです。

Direct3D との対応付けは難しいですが、おそらく OpenGL 2.0 が Direct3D 9、
OpenGL 3.0 が ShaderModel 4.0 の Direct3D 10 に近い位置づけです。
実際はここまで明確な区別が無く、もっと入り組んでいるし互換性も保たれています。
機能拡張も段階的に進められています。

例えば OpenGL 3.1 で Uniform Block / Uniform Buffer が追加されていて、
これはちょうど Direct3D 10 の ConstantBuffer に相当するものです。
OpenGL 3.2 では Geometry Shader が入りました。

GLES と GLSL ES (ESSL) は「 #ifdef GL_ES 」で区別できます。
昨日はむりやり define して動かしましたが、下記のような定義でシェーダーを共有
することにしました。

// Vertex Shader
#ifdef GL_ES
# define IN         attribute
# define OUT        varying
# define LOWP       lowp
# define MEDIUMP    mediump
precision MEDIUMP float;
precision MEDIUMP int;
#else
# define IN         in
# define OUT        out
# version 150
# define LOWP    
# define MEDIUMP   
#endif
// Fragment Shader
#ifdef GL_ES
# define IN         varying
# define LOWP       lowp
# define MEDIUMP    mediump
precision MEDIUMP   float;
precision MEDIUMP   int;
precision LOWP      sampler2D;
#else
# define IN         in
# version 150
# define LOWP    
# define MEDIUMP   
#endif

OpenGL ES のコンパイラには少々問題があって、ソースの先頭に #version が無いと
エラーになるようです。コンパイルオプションの切り替えのために複数のソースを
渡しているせいか、#version をどこに書いてもエラーとなってしまいました。
上の定義では OpenGL ES 側に #version を書いていないのはそのためです。

関連エントリ
OpenGL 3.2 の GeometryShader
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 シェーダー管理

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 シェーダー管理

OpenGL のはじめ方

Windows 上で DirectX ばかり使ってきたので、いざ OpenGL に手をつけてみようと思っても
何を使えばいいのか良くわかりませんでした。DirectX10/11 はすでにシェーダーのみとなって
いるため、OpenGL 入門によく載っているようなレガシーな命令もちょっと違います。
WindowsSDK に付属している gl.h ヘッダは OpenGL v1.1 ですが、すでに OpenGL は
3.2 まで進化しているようです。

Windows の場合、OS との間を取り持つ wgl~ という API 群があります。
wgl と gl は、Direct3D でいえば DXGI と D3D の関係に似ています。

MSDN Win32 Extensions to OpenGL

以前 Maya plug-in を作ったときも GL_ARB_vertex_program など OpenGL の
Extension 関数を呼び出すために wglGetProcAddress() を使いました。
Core API の場合も全く同じ方法でした。

(1) 最新のヘッダファイルを入手する
(2) wgl 命令で描画の初期化
(3) ヘッダの定義を元に wglGetProcAddress() でアドレスを取り出す

OpenGL Registry

上のページに gl3.h や glext.h, wglext.h 等が登録されています。
関数インターフェースの定義はこの中で行われており、必要ならプロトタイプ宣言もしてくれます。
wglGetProcAddress() を用いて関数名から実際のエントリアドレスを取得すれば
API を呼び出せるようになります。

たとえば gl3.h の中では、OpenGL 2.0 の命令として glCreateShader() が定義されています。

#define APIENTRY
#define APIENTRYP APIENTRY *
#define GLAPI extern

GLAPI GLuint APIENTRY glCreateShader (GLenum);
~
typedef GLuint (APIENTRYP PFNGLCREATESHADERPROC) (GLenum type);

上がプロトタイプ宣言、下が関数ポインタの型宣言です。APIENTRYP はただの ‘*’。
今回はエントリアドレスを直接取り出すため、下の関数ポインタ型の宣言しか使いません。
プロトタイプ宣言はデフォルトで無効になっています。

この glCreateShader() を呼び出すには下記のようにすればよいわけです。

// 取り出し
PFNGLCREATESHADERPROC glCreateShader=
        (PFNGLCREATESHADERPROC)wglGetProcAddress( "glCreateShader" );

// 呼び出し
glCreateShader( shader );

もちろん使えるのはディスプレイドライバが対応している場合だけです。
ドライバが対応していなければ wglGetProcAddress() はエラーを返します。

このように関数名からエントリアドレスを取得するやりかたは DLL と同じです。
DirectX の場合は COM なので、関数単独ではなく class 単位で API を取り出しますが
基本は同じです。

上の例のように、OpenGL 1.2~3.2 の膨大な関数を手作業で追加するのは酷です。
自動化します。

ヘッダのプロトタイプ宣言から、必要な関数名だけ抜き出すスクリプトを作っておきます。

import  re
import  sys
mp_api= re.compile( "^GLAPI\s+[a-zA-Z0-9_*]+\s+APIENTRY\s+(.*)\s+\(" )
def ProcList( filename ):
    f= open( filename, 'r' )
    for line in f:
        pat_api= mp_api.search( line )
        if pat_api != None:
            func= pat_api.group(1)
            print "GLDEFPROC( PFN"+ func.upper() + "PROC, " + func + " )"
    f.close()
ProcList( sys.argv[1] )

関数名を全部大文字にして PFN ~ PROC で囲めばインターフェースの型宣言になります。
上のスクリプトは gl3.h ヘッダから関数名だけ抜き出して下記のような一覧を生成します。

GLDEFPROC( PFNGLATTACHSHADERPROC, glAttachShader )
GLDEFPROC( PFNGLBINDATTRIBLOCATIONPROC, glBindAttribLocation )
GLDEFPROC( PFNGLCOMPILESHADERPROC, glCompileShader )
GLDEFPROC( PFNGLCREATEPROGRAMPROC, glCreateProgram )
GLDEFPROC( PFNGLCREATESHADERPROC, glCreateShader )
GLDEFPROC( PFNGLDELETEPROGRAMPROC, glDeleteProgram )
GLDEFPROC( PFNGLDELETESHADERPROC, glDeleteShader )
GLDEFPROC( PFNGLDETACHSHADERPROC, glDetachShader )
~

そのまま gl3.h を渡すと OpenGL 1.1 など余計な関数まで再定義してしまうので、
必要な 1.2~3.2 部分だけ切り出してから与えることにします。
Core API だけでなく拡張命令も同じように追加できるでしょう。

出力ファイル名を GLExtensionList.inc としておきます。
これを C ソースで include し、GLDEFPROC() を用途に応じて再定義してして使い分けます。

ヘッダ側

// GLFunction.h
#include  
#include  
#include  

#define GLDEFPROC(type,name)     extern type name;
# include  "GLExtensionList.inc"
#undef  GLDEFPROC

extern void InitOpenGL();

ソース側

// GLFunction.cpp

#define GLDEFPROC(type,name)      type name;
# include    "GLExtensionList.inc"
#undef  GLDEFPROC

struct T_ProcList {
    const char* Proc;
    void**      Addr;
};

static T_ProcList   ProcList[]= {

#define GLDEFPROC(type,name)      { #name, (void**)&name },
# include    "GLExtensionList.inc"
#undef  GLDEFPROC

    {   NULL,   NULL,   },
};

void InitOpenGL()
{
    const T_ProcList* tp= ProcList;
    for(; tp->Proc ; tp++ ){
        *tp->Addr= wglGetProcAddress( tp->Proc );
        if( !*tp->Addr ){
            int errcode= GetLastError();
            ERROR_UTF8( "'%s' not found %d/%x\n", tp->Proc, errcode, errcode );
            //*tp->Addr= _UnknownAPI_Func;
        }
    }
}

これで GLFunction.h を include すれば、OpenGL 3.2 までの命令をそのまま
使えるようになります。追加 lib は WindowsSDK の OpenGL32.lib です。
使用可能なのはドライバが対応している場合だけ。
また起動時は最初に一度 InitOpenGL() を呼び出しておく必要があります。

OpenGL ES 2.0 用に作成した描画コードが、一カ所直しただけでそのままコンパイルできました。
GLSL のシェーダーコードも共有しており、きちんど動作します。
ドライバと環境は GeForce 9800GT + 190.62 + Windows7 x64 。
モデルデータもテクスチャも GL ES 2.0 と全く同じ物です。
修正したのは下記の点

glClearDepthf( depth ); // GL ES のみ
    ↓
glClearDepth( depth );

OpenGL 3.2 に厳密に従うには、ピクセルフォーマットなどテクスチャの読み込み部も
手を入れる必要がありそうです。

関連エントリ
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 シェーダー管理