JavaScript側の処理
WebGLで必要となる基本的な処理をそれぞれ関数にまとめています。それぞれ見ていきましょう。
WebGLレンダリングコンテキストの取得
まず、WebGLレンダリングコンテキストを取得します。これは前回もやりましたね。
canvasIdという仮引数を持つinitWebGLという関数を定義しています。
const initWebGL = (canvasId)=> { const canvas = document.getElementById(canvasId); const gl = canvas.getContext("webgl2"); return gl; };
豆知識: これから主流となるJavaScript関数の記法
const initWebGL = (canvasId) => { ... };
という記法が見慣れない記法が出てきました。この書き方はJavaScriptでの新しい関数の定義方法です。
従来はfunction initWebGL(canvasId) { ... }
という記法と基本的には同じですが、関数中のthisの扱いが異なります。
従来のfunction fooFunc() { ... }
という書き方では、関数中のthisは状況(その関数が呼び出されたときにどこに所属しているか)によって変化します。
一方の新しい記法のconst fooFunc = ()=> { ... };
では状況によらず、thisはグローバルオブジェクト(ブラウザ環境ならwindow)になります。
シェーダーの準備
次に、シェーダーのコンパイルとリンク、グラフィックスハードウェア(以後、GPU)へのロードまでを行う関数です。実際のシェーダーコードは外部からvertexShaderCode仮引数とfragmentShaderCode仮引数にそれぞれ渡しています。
const initShaders = (gl, vertexShaderCode, fragmentShaderCode) => { // WebGL Program生成 const program = gl.createProgram(); // WebGL Shader生成 const vertexShader = gl.createShader(gl.VERTEX_SHADER); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); // WebGL Shaderオブジェクトにシェーダープログラム文字列をセットする。 gl.shaderSource(vertexShader, vertexShaderCode); gl.shaderSource(fragmentShader, fragmentShaderCode); // シェーダーをコンパイルする gl.compileShader(vertexShader); gl.compileShader(fragmentShader); // WebGL Programオブジェクトに頂点ShaderオブジェクトとフラグメントShaderオブジェクトをアタッチする gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); // WebGL Programをリンクする(頂点シェーダーとフラグメントシェーダーの関係などを整理して最適化する) gl.linkProgram(program); // 指定したWebGL Programの使用を宣言する gl.useProgram(program); // 頂点シェーダーコード内の"position"という変数を頂点属性の0番目として認識(バインド)させる。 gl.bindAttribLocation(program, 0, "position"); // 頂点属性の0番目を有効にする gl.enableVertexAttribArray(0); };
上記のシェーダー関連のコード行で行っていることを図に表すと、次のようになります。
頂点シェーダー、フラグメントシェーダーはそれぞれWebGLShaderというカテゴリ(TypeScriptだとまさにそういう型があります)のWebGLオブジェクトで、それをまとめているのがWebGLProgramです。1つにまとめることで扱いやすくしているのですね。
WebGLProgramに頂点シェーダー、フラグメントシェーダーをアタッチすると、次はWebGLProgramをリンク(gl.linkProgram)し、次に使用を宣言(gl.useProgram)します。
ちなみにより大きなWebGLアプリケーションになると、リンクまでやっておいて、使用については後々にとっておく場合もあります。
頂点シェーダーとフラグメントシェーダーは、どちらが欠けても正しく描画を行うことができません。
WebGL(GPU)が描画を行うためには、レンダリングパイプラインがきちんと設定されている必要があり、頂点シェーダーとフラグメントシェーダーはそれぞれレンダリングパイプラインの工程の一部を占める必須要素なのです。
ところで、次のコード部分は何を意味しているのでしょうか。
// 頂点シェーダーコード内の"position"という変数を頂点属性の0番目として認識(バインド)させる。 gl.bindAttribLocation(program, 0, "position"); // 頂点属性の0番目を有効にする gl.enableVertexAttribArray(0);
シェーダープログラムには入力が必要です。フラグメントシェーダーについては、入力がラスタライザから出力されるフラグメント(ピクセルの元となるもの。フラグメントシェーダーで計算されてピクセルになる)が入力となります。
頂点シェーダーについては、CPU側(WebGL)からの指定によってCPU側メモリから供給される頂点データがその入力となります。
ここでは、頂点シェーダー側の変数(attribute変数)と、CPU側が供給するデータの関連付けを行っています。
頂点シェーダーのコードを思い出してみましょう。
`#version 300 es in vec3 position; void main() { gl_Position = vec4(position, 1.0); }
in vec3 position;
という行がありますね。in
で始まるこの変数はattribute変数
と呼び、CPU側から供給される頂点データの1つを意味します。ここではposition、という変数名ですから、おそらく位置座標データなのでしょう。
しかし、実際に位置座標なのかどうかは、WebGL側から実際のデータと関連づけるまで分かりません。
それを関連付けるには数ステップの作業が必要です。
そのうちの1つが次の行です。
// 頂点シェーダーコード内の"position"という変数を頂点属性の0番目として認識(バインド)させる。 gl.bindAttribLocation(program, 0, "position");
実は、頂点シェーダーにおける入力頂点データには、番号が存在します。
0番、
1番、
2番、
...
とあるのです。
上のコード行は、"position"Attribute変数を0番目の頂点属性と見做すようにしてください、という命令です。
そして、頂点シェーダーで使用したい番号は、WebGL命令「gl.enableVertexAttribArray」を使ってあらかじめ有効化する必要があります。
// 頂点属性の0番目を有効にする gl.enableVertexAttribArray(0);
シェーダーと頂点データの対応付け
次に、表示対象となる3次元形状データの準備を行う関数です。実データ自体は外部からpositions仮引数に渡しています。
const initGeometry = (gl, positions) => { // バッファの確保とバインド gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); // バインド中のバッファに対して"positions"という頂点属性データをセットする。 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); // バインド中のバッファのデータ構成を伝え、それを頂点属性の0番目に関連付ける。 gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); };
そして、最後にようやく、バインド中のバッファのデータを頂点シェーダーにおける入力の頂点属性0番目("position" attribute変数が関連づけられている頂点属性番号ですね)のデータとして使用してください、という対応づけを行います。
// バインド中のバッファのデータ構成を伝え、それを頂点属性の0番目に関連付ける。 gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
図にすると次のような対応関係です。