p5.jsでwebglのshader programを自分で書いてみる

# はじめに
　やってみよう、webGL.
　ただし、p5.jsの枠組みで、です。そのためレンダラーは最初っから与えられています。キャンバスもすでに用意されています。webGLの最低限のお膳立てが済んでいる状態で、自分でシェーダープログラムを書いて描画まで行こうというのがこの記事の目的です。
　参考にしたサイト：
 　　[シェーダのコンパイルとリンク](https://wgld.org/d/webgl/w011.html)
 　　[gl_VertexID](https://wgld.org/d/webgl2/w008.html)
　実行結果：

![savevvevve.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/2315d685-f66d-2c51-0208-ef8eaece5503.png)

## コード全文
　[let's create shader program](https://openprocessing.org/sketch/2509708)
```js
// cf:シェーダーのコンパイル：https://wgld.org/d/webgl/w011.html

function setup() {
  createCanvas(400, 400, WEBGL);

  // 習うより慣れろ

  // 1.レンダラーの取得
  const gl = this._renderer.GL;

  // 2.shaderProgramの用意
  const pg = createShaderProgram(gl);

  if(pg === null){
    return;
  }

  // 3.ドローコール（今回はgl_VertexIDを使用）
  gl.clearColor(0.5, 0.5, 0.5, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.useProgram(pg);
  gl.drawArrays(gl.TRIANGLES, 0, 3);

  gl.flush();
}
function createShaderProgram(gl){
  const vs =
`#version 300 es
const float TAU = 6.28318;
const vec3 RED = vec3(1.0, 0.0, 0.0);
const vec3 GREEN = vec3(0.0, 1.0, 0.0);
const vec3 BLUE = vec3(0.0, 0.0, 1.0);
out vec3 vColor;
void main(){
  float i = float(gl_VertexID);
  vec2 p = vec2(-sin(TAU*i/3.0), cos(TAU*i/3.0));

  vColor = (i == 0.0 ? RED : (i == 1.0 ? GREEN : BLUE));

  gl_Position = vec4(p, 0.0, 1.0);
}
`;
  const fs =
`#version 300 es
precision highp float;
in vec3 vColor;
out vec4 fragColor;
void main(){
  fragColor = vec4(vColor, 1.0);
}
`;
  const vsShader = gl.createShader(gl.VERTEX_SHADER);
  gl.shaderSource(vsShader, vs);
  gl.compileShader(vsShader);

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

  const fsShader = gl.createShader(gl.FRAGMENT_SHADER);
  gl.shaderSource(fsShader, fs);
  gl.compileShader(fsShader);

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

  const program = gl.createProgram();

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

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

  return program;
}
```

## シェーダーソースを書く
　この描画の目的はグレーの背景にいつものRGB三角形を描画することです。ただしアトリビュートは高級な話題なので使いません。webgl2にはgl_VertexIDという便利な組み込みアトリビュートがあるので、これを使います。これはバーテックス番号が整数の形で与えられているものです。これをfloat化して位置を決めています。
```glsl:vert
#version 300 es
const float TAU = 6.28318;
const vec3 RED = vec3(1.0, 0.0, 0.0);
const vec3 GREEN = vec3(0.0, 1.0, 0.0);
const vec3 BLUE = vec3(0.0, 0.0, 1.0);
out vec3 vColor;
void main(){
  float i = float(gl_VertexID);
  vec2 p = vec2(-sin(TAU*i/3.0), cos(TAU*i/3.0));

  vColor = (i == 0.0 ? RED : (i == 1.0 ? GREEN : BLUE));

  gl_Position = vec4(p, 0.0, 1.0);
}
```
　正規化デバイス座標についても省略しますが、ざっくりいうと$x$が-1～1で左から右、$y$が-1～1で下から上、$z$のところはまあ、とりあえず$0$でいいです。4番目の座標には1でも入れといてください。2次元なのでこれでOKです。三角形の頂点の位置にしたいので、三角関数で位置を決めています。色はvertexIDが0,1,2なのでそれに応じて決めています。簡単ですね。
　なお、頂点の並び順は$x$軸が右、$y$軸が上の座標系において反時計回りとしています。
```glsl:frag
#version 300 es
precision highp float;
in vec3 vColor;
out vec4 fragColor;
void main(){
  fragColor = vec4(vColor, 1.0);
}
```
　varyingというのはバーテックスシェーダで決めた値をフラグメントシェーダに送るものです。今回は補間されてグラデーションが付きます。それを入れて、これで大丈夫です。透明度のところは1.0になっています。

## ソースからシェーダーを作る
　ソースができたら、シェーダーを作ります。
```js
  const vsShader = gl.createShader(gl.VERTEX_SHADER);
  gl.shaderSource(vsShader, vs);
  gl.compileShader(vsShader);

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

  const fsShader = gl.createShader(gl.FRAGMENT_SHADER);
  gl.shaderSource(fsShader, fs);
  gl.compileShader(fsShader);

  if(!gl.getShaderParameter(fsShader, gl.COMPILE_STATUS)){
    console.log("fragment shaderの作成に失敗しました");
    console.error(gl.getShaderInfoLog(fsShader));
    return null;
  }
```
　まず定数を使ってシェーダーとなるオブジェクトを作ります。そこにソース情報を与えて、コンパイルします。コンパイルできたかどうかはgetShaderParameterでわかります。ここでエラーが出た場合、プログラムは作れないので、nullを返します。失敗です。今回はうまくできたようです。

## シェーダーからプログラムを作る
　vsとfsのシェーダーができたらこれらをリンクさせてプログラムを作ります。
```js
  const program = gl.createProgram();

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

  if(!gl.getProgramParameter(program, gl.LINK_STATUS)){
    console.log("programのlinkに失敗しました");
    console.error(gl.getProgramInfoLog(program));
    return null;
  }
```
　まず定数を使ってプログラムとなるオブジェクトを作ります。ここにシェーダーをアタッチします。アタッチしたらリンクします。これも成功したか調べる方法があってgetProgramParameterといいます。失敗したらnullを返します。今回は、成功しました。

## キャンバスをクリアする
　gl.clearColorを使うとクリアする色を決められます。0～1の変数4つで指定します。この状態でclear()するとクリアされます。クリアできるものはいろいろあって、今回は色だけクリアするのでgl.COLOR_BUFFER_BITを指定すればOKです。
```js
  gl.clearColor(0.5, 0.5, 0.5, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
```

## プログラムを走らせてドローコール
　まずプログラムを走らせます。
```js
  gl.useProgram(pg);
```
　それが終わったらドローコールです。今回は最初の3つのバーテックスを元に三角形を描画するんで、gl.TRIANGLESを使います。0ははじめの数字で、3つなので3を指定します。
```js
  gl.drawArrays(gl.TRIANGLES, 0, 3);
```
　描画がすべて終わったら、おまじないとしてflush()を実行します。
```js
  gl.flush();
```
　お疲れ様でした。

# おわりに
　ここまでお読みいただいてありがとうございました。

## 参考になるサイト
　[wgld.org](https://wgld.org/)
　h_doxasさんの次のサイトは順序良くいろんなことを説明してくれるので非常にわかりやすいです。自分も数えきれないくらい参考にしました。
　ただレンダリングにおける一番最初の目標がカメラを使ったうえでの三角形の描画なのでハードルは高めかもしれません。
　いきなりuniformが登場したり、ちょっと面食らうかもしれないです。

　[WebGLの基本](https://webglfundamentals.org/webgl/lessons/ja/)
　webglについての基本の基本が書かれています。
　ラスタライズからきちんと説明してくれて分かりやすいですが基本的過ぎてハードルが高いかもです。
　アトリビュートとかテクスチャの仕組みとかについてモヤっとしたときにこれを読むと解消されるかもしれないです。

　この記事ではラスタライズについてはスルーしましたがそれはまたの機会に説明するかもしれないです。ラスタライズを説明しないことにはフラグメントシェーダが何をしているのかさっぱりなので、何とかしたいところです。ちなみにバーテックスシェーダサイドでこれに当たるのが正規化デバイス座標です。この二つはwebglの両輪で、この二つを理解することが最も重要ですね。ただ大事なことはとりあえず慣れることです。自分もwebglとかglslとかわけわからんかった頃はp5.jsのcreateShader()のレファレンスページの内容をコピペしたものをglsl部分だけいじって遊んでました。そういう経験が下地にないと高度なことは頭に入ってこないと思います。

　あとこの記事ではプログラムという言葉を多用したかもしれません。実はシェーダというのはプログラムの部品で、一般にシェーダと呼ばれているものはvsとfsという風に分かれています。その辺を意識したつもりです。