シェーダーで三角形

シェーダーを書いてみよう

my first shader

 今回はシェーダーを書いてみます。WebGLはシェーダーを書かないと始まりません。といっても三角形をひとつ描くだけです。それだけでも、それなりに手間がかかります。これを見ると分かるように青い三角形がひとつ、描画されています。やってみましょう。

コード全文

作品1のリンク

アトリビュートは使わない

 幸田露伴は動かない。
 例によってアトリビュートは使いません。あれは高度な話題なので...あれがなくても三角形くらいなら容易に描画できます。そういう方針でやるのでよろしく。代わりに使うのはドローコールです。これもおいおい解説できればと思います。

シェーダーを作る

 まずシェーダープログラムを作ります。文字列です。vec2というのは2次元のベクトルで、それの長さ3の配列を用意して頂点としています。

const vec2[3] pos = vec2[](
  vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0)
);

 正規化デバイス座標系は中心が原点で、上下左右はプラマイ1です。ここで定義しているのは左下隅、右下隅、左上隅です。この順に指定することで正の向き(反時計回り、x軸をy軸に最短で重ねられる向き)になります。gl_VertexIDは、後で頂点は3つですよ!と宣言するんですが、それにより0,1,2が入るわけです。それらに対応する頂点の位置が、これで決まります。

シェーダーをアタッチしてコンパイル

 るいぱんこ!
 まずcreateShaderで受け皿を作ります。VERTEX_SHADERなのかFRAGMENT_SHADERなのかは最初に宣言します。そこに、文字列をアタッチするわけです。アタッチしたらコンパイルします。分かりやすいですね。

  const vsShader = gl.createShader(gl.VERTEX_SHADER);
  gl.shaderSource(vsShader, vs);
  gl.compileShader(vsShader);

 フラグメントシェーダも同じようにします。2つのシェーダーができました。なおフラグメントシェーダは色を決めるシェーダと思っておけば現時点では大丈夫です。内容的にはoutでvec4形式で色を指定します。0~1で指定するので、0,0,1,1であれば不透明な青というわけですね。好きに変えてもらって大丈夫ですよ。

プログラムにアタッチしてリンキングする

 いよいよ登場しました。
 まずプログラムをcreateProgramで作ります。ここに、さっきシェーダーに文字列をアタッチしたと思いますが、全く同じようにして今度はプログラムにシェーダーをアタッチします。バーテックスもフラグメントも両方です。両方アタッチしたら、リンクします。もちろん引数はプログラムだけ。ここまでやってようやく、さっきのシェーダープログラムは使用可能なものになってくれます。

  const pg = gl.createProgram();

  gl.attachShader(pg, vsShader);
  gl.attachShader(pg, fsShader);
  gl.linkProgram(pg);

エラー処理について

 俺はミスなんかしないぞ!ジョジョーーーっ!!
 っていうんだったらいいんですが、大体バグります。なので、問題が発生した時のためにエラーキャッチは最低限用意しておきましょう。プログラム生成時のミスには大きく分けて2つあります。ひとつはシェーダー記法に違反することで起きるミス(たとえばセミコロン忘れ、間違った型宣言など)、もうひとつはプログラムリンク時のミスです(アトリビュートのロケーション被りなど)。
 シェーダーコンパイルの際のエラーは次のコードで取得できます:

  if(!gl.getShaderParameter(vsShader, gl.COMPILE_STATUS)){
    console.log("vertex shaderの作成に失敗しました");
    console.error(gl.getShaderInfoLog(vsShader));
  }

 shaderInfoLogっていうのをgetするわけですね。それでgetShaderInfoLogです。これでコンパイルをどんなふうにミスしたのか教えてくれます。次に、プログラムのリンクに失敗した場合、それを取得するコードはこちらです:

  if(!gl.getProgramParameter(pg, gl.LINK_STATUS)){
    console.log("programのlinkに失敗しました");
    console.error(gl.getProgramInfoLog(program));
  }

 今度はプログラムのログを取るのでgetProgramInfoLogというわけですね。ShaderとProgramの違い、おわかりになりましたか。
 問題が無ければprogramは使用可能になるので、早速使ってみましょう!

プログラムを使う

 まあ使うと言っても難しいことはしません。頂点の数と、それによる絵の描き方を指定するだけです。useProgramで使うプログラムを指定します。ほかのプログラムを指定するとそれで上書きされます。nullを指定すると、何にも使ってない状態になります。ドローコールの内容はgl.TRIANGLESです。「3つずつ取って三角形を描け!」ですね。

  gl.useProgram(pg);

  gl.drawArrays(gl.TRIANGLES, 0, 3);
  gl.flush();

 それで、0,3なので、「0から」「3つ」です。つまり0,1,2で三角形をひとつ描きなさいよ、という意味ですね。0,1,2はどのように位置に変換されるのかというと、バーテックスシェーダにあるように、配列のインデックスとして使われます。そして反時計回りに指定された頂点が該当し、三角形ができ、フラグメントシェーダで指定した青で描画されます。これでよし!
 もちろん背景色を設定するなら、前回やったようにclearを使います。

  gl.useProgram(pg);
  gl.clearColor(0.5,0.5,0.5,1);
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.drawArrays(gl.TRIANGLES, 0, 3);
  gl.flush();

 これで背景が灰色になります。

gray background

エラーの実際

 Qiitaの方の記事を見たらエラー出して遊んでたので、こっちでもやりましょう。そうしましょう。エラーを出すのは楽しいですからね。じゃあまずは...

#version 300 es
const vec2[4] pos = vec2[](
  vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0)
);
void main(){
  vec2 p = pos[gl_VertexID]
  gl_Position = vec4(p, 0.0, 1.0);
}

 これはバーテックスシェーダの例です。無事コンパイルエラーになります(?)。どうエラーになるかというと...

SHADER_INFO_LOG:
ERROR: 0:7: 'gl_Position' : syntax error

 と、出ていますが、gl_Positionがまずいわけではないです。その前を見てください。セミコロンが付いてないでしょ。これが原因です。初心者のつまずきポイント第一位(想像)で、glslはセミコロンが無いと容赦なくエラーを出します。文の区切りは重要ですからね。そういう理由から自分はjavascriptを書く場合でも絶対にセミコロンを省略しません。使い分けが面倒だからです。
 じゃあ次。

#version 300 es
const vec2[4] pos = vec2[](
  vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0)
);
void main(){
  vec2 p = pos[gl_VertexID];
  gl_Position = vec(p, 0.0, 1.0);
}

 これは分かりやすいですね。vecなんて型は存在しません。正しくエラーが出ます。

SHADER_INFO_LOG:
ERROR: 0:7: 'vec' : no matching overloaded function found
ERROR: 0:7: '=' : dimension mismatch
ERROR: 0:7: 'assign' : cannot convert from 'const mediump float' to 'Position highp 4-component vector of float'

 ちゃんとvecなんかないって教えてくれます。エラーログはきちんと読みましょう...
 フラグメントシェーダのエラーも見てみましょう。precision宣言を省いちゃえ!

#version 300 es
// precision highp float;
out vec4 fragColor;
void main(){
  fragColor = vec4(0.0, 0.5, 1.0, 1.0);
}
SHADER_INFO_LOG:
ERROR: 0:3: '' : No precision specified for (float)

 precision宣言を要求されます。これはfloatをどう取り扱うかというものです。自分は基本的にhighpにしていますがmediumpの方がいい場合もあるようです。詳しくは分かりません。なおp5はhighしか使っていません。バーテックスシェーダの方は基本的にhighpが使われるんですが、宣言は必須ではないので自分も用意していません。というか宣言しても無視されるらしいです。
 エラー処理は初心者に軽視されがちですが、たとえばスマホとかだとコンソールが基本的に見れないので、こういうのを取得できるとそれを使ってデバッグ機構とか作れるからありがたいんですよね。そういう側面もあります。誰が初心者だって?自分です。
 もう話すことは無いんですが、Qiitaの方でgl定数に触れてますね。じゃあちょっとだけ追加。

gl定数について

 gl.FRAGMENT_SHADERやgl.LINK_STATUSが出てきましたが、これらは定数、というか整数です。整数なので、ただの整数をこれらの代わりに使っても動きます。実はそれを利用した指定方法がいずれ登場するんですが、それはおいといて、実際にこれらの実体である整数が使われることは稀です。なので覚えておいてもあんま意味ないんですが、知っておくといいでしょう。参考までに:

  console.log(gl.VERTEX_SHADER); // 35633
  console.log(gl.FRAGMENT_SHADER); // 35632
  console.log(gl.COMPILE_STATUS); // 35713
  console.log(gl.LINK_STATUS); // 35714
  console.log(gl.COLOR_BUFFER_BIT); // 16384

 MDNのサイト:WebGLの定数
 こちらを見れば大体わかります。COLOR_BUFFER_BITのところが0x00004000になってますね。全部16進数表記です。ZEROとONEが0と1になってるように、一部は被っています(特に問題はないです)。おしりに整数が付いている定数は軒並み連番になっています。なので、0番に整数を足すと他の定数にアクセスできるようになっています。今はそれだけわかっていれば大丈夫です。ああ、Qiitaより情報量が増えてしまった...
 特に更新はしません。めんどうなので。ここで終わりですね。ではまた。いつか。

今回登場した関数

createShader
shaderSource
compileShader
createProgram
attachShader
linkProgram
getShaderParameter
getProgramParameter
getShaderInfoLog
getProgramInfoLog
useProgram