layoutとlocation

ロケーション指定

 前回ロケーションについてさらっと触れましたが、レイアウト修飾子やbindAttribLocationでレイアウトを決めるのは基本です。WebGL1ではbindAttribLocationを使わないと自由にロケーションを決められませんでした。それでは一切何も指定しなかったらどうなるのでしょう。また同じロケーションというのは可能なのか?優先順位は?そこら辺をちょっとだけ掘り下げて補足とします。なお補足だけであと2節あります。というか基礎だけで8節あるので、まあ頑張ってついてきてください...FIGHT!

コード全文

作品20のリンク

attribute location

 まあロケーションは関数名にも出てくるんですが、内実はindexです。何のindexかというと、vertexAttributeArrayのスロット番号です。これの決め方としてbindAttribLocationを使う方法とlayout修飾子を使うやり方を説明しました。WebGLも今更名称変更できないので、これからもロケーションという名前は(誤解を招くにもかかわらず)使われ続けるでしょう(今後はWebGPUの時代、WebGLは開発終了しているので)。

 なおちらっと見ただけなので詳細は分かりませんが、WebGPUではアトリビュートのロケーションがきちんとプログラムごとになっているようです。そもそもプログラムやvertexAttributeArrayに相当する概念があるのかどうか分かんないのでこれ以上の言及は避けます。自分も全く勉強してないのでわかんないです。

 それはさておき、とりあえずこの節では何にも指定しなかった場合にロケーションがどうなるのか見てみましょうね。

プログラムの解説

 様々なアトリビュートを用意します。とはいえ内容的にはx成分を加えるだとか、意味の無いものです。それでもgl_Positionの計算に寄与していればハブられないので、そこだけ注意しながら作りました。

#version 300 es
in vec2 aAttr0;
in vec4 aAttr1;
in float aAttr2;
in vec3 aAttr3;
in vec2 aPosition;
void main(){
  vec2 p = aPosition;
  // 有効化するためのダミー処理。すべて0なのでtrivial.
  p.x += aAttr2;
  p.x += aAttr0.x;
  p.x += aAttr3.x;
  p.x += aAttr1.w; // x,y,zの場合は0だがwの場合は1になります
  gl_Position = vec4(p, float(gl_VertexID)*0.0, 1.0);
}

 これとは別に、ガチガチにlayout修飾子でロケーションを決めたバージョンも用意しました。こちらは完全に確定します。

`#version 300 es
layout (location = 0) in vec2 aAttr0;
layout (location = 1) in vec4 aAttr1;
layout (location = 2) in float aAttr2;
layout (location = 3) in vec3 aAttr3;
layout (location = 4) in vec2 aPosition;
void main(){
vec2 p = aPosition;
// 有効化するためのダミー処理。すべて0なのでtrivial.
p.x += aAttr2;
p.x += aAttr0.x;
p.x += aAttr3.x;
p.x += aAttr1.w; // x,y,zの場合は0だがwの場合は1になります
gl_Position = vec4(p, float(gl_VertexID)*0.0, 1.0);
}

 それ以降のプログラムは併用した場合の実験で使うので後で説明します。とりあえずやることは、上記の1つ目でバインドをした場合としない場合の比較です。一切何もしない場合、ロケーションはどうなるのでしょうか。なお、プログラムに紐付けられたattributesからそれが分かるので、それを使って調べています。

  const pg = createShaderProgram(gl, {vs, fs});
  const attrs = pg.attributes;
  attrText += `pgのアトリビュート\n`;
  attrText += `aAttr0: ${attrs.aAttr0.location}\n`;
  attrText += `aAttr1: ${attrs.aAttr1.location}\n`;
  ...

 なお、スマホでも確かめられるようにHTMLをいじって画面に表示されるようにしました。いろんなブラウザで試してみましょう。

環境依存ロケーション

 先に言うと、これは環境依存です。まずMicrosoftEdgeやFirefox系の場合、宣言した順に並びます。

pgのアトリビュート
aAttr0: 0
aAttr1: 1
aAttr2: 2
aAttr3: 3
aPosition: 4

 ところが、私のAndroidスマホでは次のようになります:

pgのアトリビュート
aAttr0: 2
aAttr1: 4
aAttr2: 1
aAttr3: 3
aPosition: 0

 これは何の番号かというと、まあシェーダーを見ると分かるんですが、「登場順」です。aPositionが最初に現れるため、0が付与されるのでしょう。そんなわけで、何にも指定せずに4だと思ってコードを書くとえらいことになります。
 きちんとバインドやlayoutを使えば、然るべきロケーションになるので、そうしましょう。後の2つのプログラムはそれを表現したものになっています。実際、どの環境でもロケーションが順に並んでいるのが確認できるかと思います。
 この話題はこれで終わりです。次に行きましょう。

ロケーションの上書き

 それではこれらを両方使うとどうなるんでしょうか。実は、layout修飾子によるロケーション設定が優先です。つまり上書き不可です。

#version 300 es
in vec3 aWater;
layout (location = 1) in vec3 aFish;
layout (location = 4) in vec2 aPosition;
in vec2 aSealion;
in vec2 aFarseal;
void main(){
  vec2 p = aPosition;
  p.x += aFurSeal.x;
  p.y += aSealion.y;
  p += aFish.xy;
  p += aWater.yz;
  gl_Position = vec4(p, 0.0, 1.0);
}

 aFishとaPositionを1と4で固定します。これによりaFishとaPositionは1と4で確定するので、次のようにbindで上書きしようとしても失敗します:

  // layoutがshaderで指定されている場合、bindAttribLocationは無視されるのを確かめる。
  // なおここでaPositionをaFishと同じ3にしてもエラーは出ない。layoutで上書きされるので。
  const pg_test0 = createShaderProgram(gl, {vs:vs_test0, fs:fs, layout:{aPosition:0, aFish:3, aWater:2}});

 ここにも書きましたが、この場合aFishとaPositionは設定が無視されるので、何を指定しても無意味です。同じ値でも問題なく無視されます。aWater, これはlayoutで指定していないので採用され、2になります。じゃあ残りのaSealionとaFursealはどうなるかというと、これらは自動的に割り当てられます。小さい方から順に埋まる仕組みで、規則はさっき述べた感じになります。Chromeなどでは、

pg_test0 aWater: 2
pg_test0 aFish: 1
pg_test0 aPosition: 4
pg_test0 aSealion: 0
pg_test0 aFurseal: 3

aSealionの方が先に宣言されているので、残った0,3のうちaSealionが0でaFursealが3になるんですが、私のスマホでは、

pg_test0 aWater: 2
pg_test0 aFish: 1
pg_test0 aPosition: 4
pg_test0 aSealion: 3
pg_test0 aFurseal: 0

こうなりますね。aFursealがプログラム内で先に出てくるからだと思います。こっちが0になるわけですね。面白いですね。

ロケーションの競合

 それではすでに定まったロケーションと同じ値を設定するとどうなるんでしょう。なお、layout修飾子で同じ値を設定した場合、普通にリンクに失敗することを先に述べておきます。致命的なエラーが出ます。それではbindで既に与えられている数字と同じ値を設定したら?

#version 300 es
in vec3 aFish;
layout (location = 0) in vec2 aPosition;
void main(){
  vec2 p = aPosition;
  p += aFish.xy;
  gl_Position = vec4(p, 0.0, 1.0);
}

 aPositionを0にしてありますが、aFishは未定です。次のようにして同じ値にしようとします。

  // bindAttribLocationでロケーションを衝突させる実験
  const pg_test1 = createShaderProgram(gl, {vs:vs_test1, fs:fs, layout:{aFish:0}});

 当然ですがリンクに失敗します。ですがそのメッセージは環境により異なります。まずEdgeやChromeの場合:

Attribute 'aPosition' aliases attribute 'aFish' at location 0

 普通ですね。次に、Firefox系の場合:

Attribute 'webgl_29688de933f0bd7a' aliases attribute 'webgl_e9582f3168894ae' at location 0

 なんじゃこりゃ...そんな名前のattribute用意してませんが...どうやら内部的に使用する際のコードネームみたいな何かなんでしょうか。わからんですね。最後にわがスマホAndroidの場合:

Error: Input aFish location or component conflict with others.

 aFishにしか言及していないですね。「or component」とあるので行列コンフリクトにも触れていそうですが。まあいいか。これで全部かと思います。他にも色々あるんでしょうが持ってないのでわかんないです。iPhoneだとまた違うんだろうか?とにかくいずれにせよリンクはできないので、ロケーションはきちんと設定しましょう。
 そうそう、ちらっと触れましたが行列attributeの場合というのがあって、mat mやmat mxnの場合、m個のスロットが連番で予約されるんですよね。詳しくはいずれやるかと思います。予約されるため、当然競合するとリンキングエラーとなります。

次回予告

 次はnormalize引数について触れられればと思います。また次回。おたのしみに。

三角形がずれてるけど...

 0しか足してないはずなのに?ああここですね。

  p.x += aAttr2;
  p.x += aAttr0.x;
  p.x += aAttr3.x;
  p.x += aAttr1.w; // x,y,zの場合は0だがwの場合は1になります

 いずれやりますが、aAttr1はvec4ですがデータを供給していません。最後のドローコールではaPositionのロケーションが4で確定しているので、そこに頂点データを放り込んで三角形を描いています。それ以外は一切供給していません。にもかかわらずこのaAttr1.wは「1」です。その仕組みについてはいずれ、解説できるかと思います。