8. 頂点シェーダ

頂点シェーダでは入力された頂点属性データに座標変換や陰影処理などを行います。3DS は 4 つの 24 ビット浮動小数点数で構成されたベクトル型データを扱うことのできる頂点プロセッサを 4 基搭載しています。入力された頂点データは、4 つある頂点プロセッサによって並列に処理されます。プロセッサでは汎用的な計算をすることができますが、VRAM からのデータの読み込みと書き込みはできません。

OpenGL ES 2.0 と同様に、3DS で扱われる頂点データには"座標"や"法線"といった明確な区別はありません。頂点処理シェーダが入力されたデータをどのように処理し、出力するかで属性が決定します。一般的に、3D グラフィックス処理で入力される頂点データには以下のような属性があります。

  • 頂点座標
  • 法線ベクトル
  • 接線ベクトル
  • テクスチャ座標
  • 頂点カラー

 

頂点シェーダは PICA グラフィックスコア独自のアセンブリ言語で記述します。この章の内容を読み進めるにあたっては、「頂点シェーダ リファレンスマニュアル」との併読をお勧めします。

8.1. 頂点データの入力

アプリケーションから入力される頂点データは、頂点属性番号に関連付けられた入力レジスタを介して頂点シェーダに渡されます。頂点シェーダ側では #pragma bind_symbol により、データ名とデータを入力するレジスタを指定します。

コード 8-1. データ名とレジスタの関連付け(シェーダアセンブラ)
#pragma bind_symbol(AttribPosition.xyzw, v0, v0)

上記のコード例では、入力レジスタ v0 のコンポーネント xyzw にデータ名 "AttribPosition" を関連付けています。同じ入力レジスタに複数のデータ名を関連付けることはできません。また、第 2、第 3 引数は同じ値(もしくは第 3 引数を省略して指定)でなければなりません。

アプリケーション側では glBindAttribLocation() で頂点属性番号とデータ名を関連付け、glEnableVertexAttribArray() で頂点属性番号の関連付けを有効にし、glVertexAttribPointer() などで頂点データを入力します。

コード 8-2. 頂点属性番号の関連付けと頂点データ入力
glBindAttribLocation(program, 0, "AttribPosition");
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, pointer);

上記のコード例では、頂点属性番号 0 を "AttribPosition" の名前を持つデータに関連付け、4 成分の頂点属性を入力しています。レジスタの番号と頂点属性番号は同じである必要はありません

#pragma bind_symbol で関連付けられていない入力レジスタの値は不定です。入力される頂点データが GL_FLOAT 型でない場合、自動的に GL_FLOAT 型への変換が行われます。ただし、データの"normalize"(正規化)は行われませんので注意が必要です。

頂点バッファを使用する場合、頂点属性のデータタイプとデータサイズの組み合わせが頂点データの転送速度に影響します。詳細については、「15.13. 頂点属性の組み合わせによる頂点データの転送速度への影響」を参照してください。

注意:

glVertexAttribPointer() で頂点データを入力する場合、第 4 引数の指定(正規化の指定)はハードウェアでサポートされていないため無視されます。指定が反映されないため、頂点シェーダ内で明示的に正規化処理を行う必要があります。

glVertexAttribPointer()typeGL_FIXED および GL_UNSIGNED_SHORT を指定することはできません。また、GL_FLOATGL_SHORT を指定した場合、ptr に指定するポインタはアライメントがそれぞれ 4 Byte、2 Byte でなければなりません。

頂点シェーダでは 1 つの頂点を処理する間に、少なくとも 1 つの入力レジスタ(1 コンポーネントでもかまいません)から頂点データを読み込まなければ正常に動作しない可能性があります。

8.2. 頂点データの出力

頂点シェーダの処理結果は、出力頂点属性にマッピングされた出力レジスタへの書き込みで後段のプロセスへ渡されます。頂点シェーダ側では #pragma output_map により、出力頂点属性名とデータを出力するレジスタを指定します。

コード 8-3. 出力頂点属性のレジスタマップとレジスタへの書き込み(シェーダアセンブラ)
#pragma output_map(position, o0)
mov     o0,     v0

上記のコード例では、出力レジスタ o0 を頂点座標にマップしています。o0 にデータを書き込むことで、データが頂点座標として出力されます。

後段のフラグメント処理には予約されたシェーダしか使用できないため、頂点シェーダが出力する頂点属性はあらかじめ決められたものになります。出力頂点属性には以下の属性名があり、出力される属性には頂点シェーダで設定すべき成分が決められています。

表 8-1. 出力頂点属性

属性名

出力される属性

設定すべき成分

position

頂点座標

(x y z w) の 4 成分

color

頂点カラー

(R G B A) の 4 成分

texture0

テクスチャ座標 0

(u v) の 2 成分

texture0w

テクスチャ座標 0

(w) の 1 成分

texture1

テクスチャ座標 1

(u v) の 2 成分

texture2

テクスチャ座標 2

(u v) の 2 成分

quaternion

クォータニオン

(x y z w) の 4 成分

view

ビューベクタ

(x y z) の 3 成分

generic

汎用属性

ジオメトリシェーダで使用する任意の数の成分

頂点シェーダでは、マッピングされたレジスタすべてのコンポーネント(xyzw)に対して何らかの値を書き込まなければなりません。そのため、#pragma output_map で指定されたレジスタのコンポーネントすべてがマッピングされていない場合、使用されていないコンポーネントにダミー値を書き込むなどの処理が必要になります。

また、マッピングされたレジスタすべてに値が書き込まれた時点で頂点シェーダは処理を強制的に終了し、次の頂点処理に移ります(end 命令の呼び出しは必要です)。つまり、最後の属性データがレジスタに書き込まれた後の命令は実行されない可能性があります。

1 つの出力レジスタ(コンポーネントごと)への書き込みは 1 頂点の処理の間に 1 度だけです。同じ出力レジスタの同じコンポーネントへ複数回書き込みを行った場合の動作は保証されていません。

generic 以外の出力頂点属性をマッピングできる出力レジスタは 7 つまでです。generic 以外で 8 つ以上の出力頂点属性をマッピングする場合は、複数の頂点属性を 1 つのレジスタにマッピングする必要があります。その場合、texture1texture2o0.xyo0.zw にマッピングするといったように、複数の頂点属性(合計が 4 成分までのもの)を 1 つのレジスタにパックしなければなりません。

8.3. ユニフォーム設定

#pragma bind_symbol で入力レジスタ以外(浮動小数点数レジスタ、ブールレジスタ、整数レジスタ)にデータ名を関連付けている場合、glUniform*() によってアプリケーションから各レジスタに値を設定することができます。glUniformMatrix*()transposeGL_TRUE を指定することで、行列の転置(column-major order から row-major order への変換)を関数内で行うことができます。

以下に、頂点シェーダ側とアプリケーション側のコード例を示します。

コード 8-4. 入力レジスタ以外へのデータ名関連付けの例(シェーダアセンブラ)
#pragma bind_symbol ( ModelViewMatrix , c0 , c3 )
#pragma bind_symbol ( LoopCounter0 , i1 , i1 )
#pragma bind_symbol ( bFirst , b2 , b2 )
#pragma bind_symbol ( Scalar.x , c4, c4 )
コード 8-5. ユニフォーム設定の例
uniform_location = glGetUniformLocation ( program , "ModelViewMatrix" );
GLfloat matrix[4][4];
glUniform4fv ( uniform_location , 4 , matrix );

GLfloat scalar_value;
uniform_location = glGetUniformLocation ( program , "Scalar" );
glUniform1f ( uniform_location , scalar_value );

uniform_location = glGetUniformLocation ( program , "bFirst" );
glUniform1i ( uniform_location , GL_TRUE );

GLint loop_setting[3] = { 4 , 0 , 1 } ; // loop_count-1 , init , step
uniform_location = glGetUniformLocation ( program , "LoopCounter0" );
glUniform3iv ( uniform_location , loop_setting );

上記のコード例では、"Scalar" のデータ名にコンポーネント x を指定しています。このように、関連付けるレジスタが浮動小数点数レジスタである場合はコンポーネントを指定することができます。コンポーネントの指定は、xyzw の順序で行い、連続する成分でなければなりません。つまり、xyzwyzw は指定可能ですが、xzywxyw は指定することができません。

整数レジスタはシェーダプログラムの loop 命令の制御に使用されます。24 ビット幅のレジスタで、0 ~ 7 bit がループ回数、8 ~ 15 bit が初期値、16 ~ 23 bit が増加値に割り当てられています。loop 命令はループカウンタレジスタを初期値で初期化し、loop 命令から endloop 命令までを(指定されたループ回数 + 1)回繰り返し実行します。ループを繰り返すごとにループカウンタレジスタが増加値分だけ増加します。

8.4. クリップ座標系の注意事項

頂点シェーダが出力するクリップ座標系の Z 成分は OpenGL ES 標準の仕様と異なっています。

OpenGL ES では -Wc から Wc の範囲でクリップされますが、3DS では 0 から -Wc の範囲(符号が反転しています)でクリップされます。このため、アプリケーションが OpenGL ES 互換の射影変換行列を使用する場合、-Wc から Wc の範囲を 0 から -Wc の範囲に変換する処理が必要になります。

アプリケーションで射影変換行列を変換する場合

以下のような変換を施してからユニフォーム設定されたレジスタに設定します。

コード 8-6. OpenGL ES 互換の射影変換行列を変換する
GLfloat projection[16];
projection[2] = (projection[2] + projection[3]) * (-0.5f);
projection[6] = (projection[6] + projection[7]) * (-0.5f);
projection[10] = (projection[10] + projection[11]) * (-0.5f);
projection[14] = (projection[14] + projection[15]) * (-0.5f);
頂点シェーダ側で行う場合

射影変換処理を以下のようにして行います。

コード 8-7. OpenGL ES 互換の射影行列で射影変換する(シェーダアセンブラ)
#pragma output_map(position, o0)
#pragma bind_symbol(attrib_position, v0)
#pragma bind_symbol(modelview, c0, c3)
#pragma bind_symbol(projection, c4, c7)
def     c8, -0.5, -0.5, -0.5, -0.5

// Model View Transformation
dp4     r0.x, v0, c0
dp4     r0.y, v0, c1
dp4     r0.z, v0, c2
dp4     r0.w, v0, c3
// Projective Transformation
dp4     o0.x, r0, c4
dp4     o0.y, r0, c5
mov     r1, c6
add     r1, r1, c7
mul     r1, r1, c8
dp4     o0.z, r0, r1
dp4     o0.w, r0, c7

8.5. 頂点キャッシュ

頂点シェーダで生成または加工された頂点データの一部はキャッシュとして保存されます。頂点インデックスをもとに、頂点シェーダに入力される頂点データがキャッシュに存在する頂点データの元となったデータと同じであると判断された場合、頂点シェーダで処理をせずにキャッシュ上に存在する処理済みの頂点データを次プロセスへと送ります。一般的に GL_TRIANGLES で頂点データを入力すると、同じ頂点データに対して複数回の処理が行われることが多いのですが、処理済みの頂点データがキャッシュ上に存在する場合は処理を省略することができます。

頂点キャッシュを使用するためには、以下の条件を満たしていなければなりません。

  • 頂点インデックスを参照した頂点データの入力形式であること。つまり、glDrawElements() を呼び出して頂点データを入力していること。
  • 頂点バッファを使った頂点データの入力であること。

頂点キャッシュには 32 エントリの頂点データをキャッシュすることができ、LRU の挙動に似た、独自のアルゴリズムで実装されています。

繰り返し参照される頂点データが 32 頂点以内に収まっているときは、キャッシュにヒットする可能性が高くなります。頂点キャッシュの効率は、インデックスアレイが格納されているメモリの使用状況や、頂点シェーダで実行されているシェーダの長さなど、インデックスの並び以外の条件も影響します。そのため、最適なインデックスはコンテンツに依存し、一意には決まらない可能性があります。

8.6. 頂点シェーダへの問い合わせ

頂点シェーダに対して、アクティブ状態の頂点属性とユニフォームの情報を問い合わせることができます。

8.6.1. 頂点属性情報の取得

頂点シェーダに入力される頂点データの属性情報を glGetActiveAttrib() で取得することができます。

コード 8-8. glGetActiveAttrib() の定義
void glGetActiveAttrib(GLuint program, GLuint index, GLsizei bufsize, 
                       GLsizei* length, GLint* size, GLenum* type, char* name);

リンク済みでないなど、不正なプログラムオブジェクトが program に指定された場合は GL_INVALID_OPERATION エラーが生成されます。

index には、program で指定したプログラムオブジェクトに対して glGetProgramiv()pnameGL_ACTIVE_ATTRIBUTES を渡して取得した頂点属性の数 - 1 までの 0 以上の値を指定します。負の値や頂点属性の数以上の値を指定した場合は GL_INVALID_VALUE エラーが生成されます。

bufsize には name に指定した配列のサイズを指定してください。負の値を指定した場合は GL_INVALID_VALUE エラーが生成されます。

type には頂点属性の種別が格納されます。size には頂点属性のサイズ(頂点属性を表現するのに必要とされる、type で示された種別単位での個数)が格納されます。

name には頂点属性の名前が格納されます。頂点属性の名前の文字数が bufsize よりも大きい場合は bufsize - 1 文字までが格納され、終端文字(NULL)が末尾に追加されます。length には name に格納された文字数(終端文字は含みません)が格納されます。

8.6.2. ユニフォーム情報の取得

プログラムオブジェクトに登録されているユニフォーム情報を glGetActiveUniform() で取得することができます。頂点シェーダだけに限らず、後述するジオメトリシェーダや予約フラグメントシェーダで使用するユニフォームの情報についても取得することができます。

コード 8-9. glGetActiveUniform() の定義
void glGetActiveUniform(GLuint program, GLuint index, GLsizei bufsize, 
                        GLsizei* length, GLint* size, GLenum* type, char* name);

リンク済みでないなど、不正なプログラムオブジェクトが program に指定された場合は GL_INVALID_OPERATION エラーが生成されます。

index には、program で指定したプログラムオブジェクトに対して glGetProgramiv()pnameGL_ACTIVE_UNIFORMS を渡して取得したユニフォーム情報の数 - 1 までの 0 以上の値を指定します。負の値やユニフォーム情報の数以上の値を指定した場合は GL_INVALID_VALUE エラーが生成されます。

bufsize には name に指定した配列のサイズを指定してください。負の値を指定した場合は GL_INVALID_VALUE エラーが生成されます。

type にはユニフォームに設定する値の種別が格納されます。size には設定値のサイズ(ユニフォーム設定値に必要とされる、type で示された種別単位での個数)が格納されます。例えば、モデルビュー変換行列のような 4x4 行列の場合、type には GL_FLOAT_VEC4 が、size には 4 がそれぞれ格納されます。

name にはユニフォームの名前が格納されます。ユニフォームの名前の文字数が bufsize よりも大きい場合は bufsize - 1 文字までが格納され、終端文字(NULL)が末尾に追加されます。length には name に格納された文字数(終端文字は含みません)が格納されます。

8.6.3. 設定値の種別について

glGetActiveAttrib()glGetActiveUniform() で取得することのできる値の種別には、以下のものがあります。

表 8-2. 設定値の種別一覧

種別

コンポーネント数

主な使用項目

GL_FLOAT

float

1

バイアス値、スケール値

GL_FLOAT_VEC2

float

2

ビューポート設定

GL_FLOAT_VEC3

float

3

カラー(RGB)、方向ベクトル

GL_FLOAT_VEC4

float

4

カラー(RGBA)、変換行列

GL_INT

int

1

モード設定

GL_INT_VEC3

int

3

コンバイナのソース入力

GL_BOOL

bool

1

有効・無効の指定

GL_SAMPLER_1D

int

1

参照テーブルの指定

※ 実際に各シェーダのユニフォームで使用されている種別の一覧です。

8.7. 複数のユニフォームに対する設定および取得

3DS では、複数のユニフォームを一括で設定・取得する関数を提供しています。

glUniformsDMP() の呼び出しで、現在バインドされているプログラムオブジェクトの複数のユニフォームに対して値を一括で設定することができます。

コード 8-10. ユニフォームの一括設定
void glUniformsDMP(GLuint n, GLint* locations, GLsizei* counts, 
                   const GLuint* value);

n には設定を行うユニフォームの個数を指定します。

locations には n 個のユニフォームのロケーション(glGetUniformLocation() で取得)を格納した配列へのポインタを、counts には n 個のユニフォームの要素数を格納した配列へのポインタを指定します。ユニフォームの要素数は glUniform*()count に相当し、配列型ユニフォームに対しては設定する配列の要素数を、配列型ではない場合は 1 を counts で指定する配列に格納してください。

value にはユニフォームに設定する値を格納した配列へのポインタを指定します。ユニフォームごとにデータの個数は異なるため、value には locationscounts と同じインデックスで対象の設定値を格納するとは限りません。設定するユニフォームには GLfloat 型データと GLuint 型データの両方を混在することができます。GLfloat 型のユニフォームに設定する値は、GLfloat 型データを 32 ビットデータとして格納してください。

この関数ではエラーチェックは行われません。すべての引数に関して、不正な値を指定した場合の動作は不定です。

glGetUniformsDMP() の呼び出しで、指定したプログラムオブジェクトの複数のユニフォームに設定されている値を一括で取得することができます。

コード 8-11. ユニフォームの一括取得
void glGetUniformsDMP(GLuint program, GLuint n, GLint* locations, 
                      GLsizei* counts, GLuint* params);

program にはユニフォームの値を取得するプログラムオブジェクトを指定します。

n には取得をするユニフォームの個数を指定します。

locations には n 個のユニフォームのロケーション(glGetUniformLocation() で取得)を格納した配列へのポインタを、counts には n 個のユニフォームの要素数を格納した配列へのポインタを指定します。ユニフォームの要素数には、配列型ユニフォームに対しては値を取得する配列の要素数を、配列型ではない場合は 1 が格納されます。

params にはユニフォームの値を取得する配列へのポインタを指定します。ユニフォームごとにデータの個数は異なるため、params には locationscounts と同じインデックスに対象の設定値が格納されているとは限りません。値を取得するユニフォームには GLfloat 型データと GLuint 型データの両方を混在させることができます。params には GLfloat 型のユニフォームから取得した GLfloat 型データは 32 ビットデータとして格納されています。

この関数ではエラーチェックは行われません。すべての引数に関して、不正な値を指定した場合の動作は不定です。

8.8. その他の注意点など

glGetUniformLocation() で取得することのできるユニフォームのロケーションは、glUniform*()glGetUniform*() によるユニフォーム値へのアクセスに使用することができます。このとき、ロケーションにオフセット値を加えることで、配列型ユニフォームの要素を指定してアクセスすることができます。つまり、ロケーションに 1 を加えた場合、配列型ユニフォームの 2 番目の要素にアクセスすることになります。

ユニフォームのロケーションは glLinkProgram() の呼び出し時に値が決定しますので、本来はプログラムオブジェクトごとに値が異なります。glUniform*() では、ロケーションが関連するプログラムオブジェクトがカレントのプログラムとして設定されていなければエラーを生成します。glGetUniform*() では、ロケーションが関連するプログラムオブジェクトが program と異なる場合にエラーを生成します。ただし、予約フラグメントシェーダのユニフォームに限り、ロケーションの値に 0xFFF80000 を論理和で指定した glUniform*() および glGetUniform*() ではエラーが生成されません。

出力する必要のない頂点属性を #pragma output_map の定義に含めないようにしてください。頂点シェーダの実行時には、定義された頂点属性(出力レジスタ)に対して必ず書き込みをしなければならないため、無駄な命令が必要になるからです。また、出力属性によっては GPU の回路の一部に対してクロック制御が行われるため、無駄なバッテリー消費にもつながってしまいます。

頂点バッファを使用しない描画の頂点属性は最大 16 個、頂点バッファを使用する描画の頂点属性は最大 12 個です。ただし、頂点バッファを使用する描画で 12 個の頂点属性を使用する場合は、頂点データの配置に関する制約(「6.8.1. glDrawElements() のみの制約」参照)に注意してください。

頂点シェーダアセンブラでは、常に頂点属性の個数を 16 個まで定義できますが、上記の条件に従って決定される最大個数を超えた場合、描画関数の呼び出し時に GL_INVALID_OPERATION のエラーが生成される可能性があります。