三角形を描画するスクリプトを通してWebGL2の基礎を理解しましょう。
はじめに
WebGL2とはざっくりいうとJavaScriptからOpenGLの機能を利用することができるJavaScriptAPIです. ブラウザの機能なのでプラグインなどが必要なく、既存のOpenGLのコードをかなり流用できます*1。
WebGL2があるからにはWebGL(無印)もあるわけですが、せっかくなら新しい方を使おうということでこの記事ではWebGL(無印)に関しては一切触れません。
また、WebGL2を「完全に理解」するためには広範な知識が必要になります。したがって、この記事でカバーする部分とカバーしない部分を明確にしておきましょう。
この記事で扱うこと
- シェーダープログラムをコンパイルする方法
- 頂点バッファーオブジェクト(VBO)の利用方法
- 頂点配列オブジェクト(VAO)の利用方法
この記事で扱わないこと
- GLSLの書き方
- インデックスの頂点バッファオブジェクトの利用方法
- Canvasを画像化してダウンロードする方法
- vertexAttribPointer()の引数の詳しい説明
コードの全貌
いきなりですが、以下が完成したコードになります。
<!-- index.html --> <head> <style> canvas{ width: 512px; height:512px; } </style> </head> <body> <canvas id="canvas"></canvas> <script src="script.js"></script> </body>
// script.js "use strict"; let vertexShaderSource = `#version 300 es in vec4 position; in vec4 color; out vec4 vcolor; void main() { vcolor = color; // gl_Positionは必須 // out変数としての定義不要 gl_Position = position; } `; let fragmentShaderSource = `#version 300 es // データ型指定必須 precision highp float; // フラグメントシェーダーは最終的なout変数を定義する必要あり in vec4 vcolor; out vec4 outColor; void main() { outColor = vcolor; } `; function createShader(gl, type, source) { let shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); if (success) { return shader; } console.log(gl.getShaderInfoLog(shader)); // eslint-disable-line gl.deleteShader(shader); return undefined; } function createProgram(gl, vertexShader, fragmentShader) { let program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); let success = gl.getProgramParameter(program, gl.LINK_STATUS); if (success) { return program; } console.log(gl.getProgramInfoLog(program)); // eslint-disable-line gl.deleteProgram(program); return undefined; } function main() { // コンテクスト取得 let canvas = document.querySelector("canvas"); let gl = canvas.getContext("webgl2"); if (!gl) { return; } // バーテックスシェーダーとフラグメントシェーダーをコンパイル let vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); let fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // バーテックスシェーダーとフラグメントシェーダーをプログラムとリンクする let program = createProgram(gl, vertexShader, fragmentShader); /******************************************************************************** */ gl.bindAttribLocation(program, 0, "position"); gl.bindAttribLocation(program, 1, "color"); const tri1 = [ // x, y, z 1, 1, 0, -1, -1, 0, 1, -1, 0, 0, 0.75, 0, 1, 0, 0.75, 0, 1, // Right Down 0, 1, 0, 1 ]; const tri2 = [ // x, y, z, r, g, b, a -1, 1, 0, -1, -1, 0, 1, 1, 0, // r, g, b, a // Left Up 0, 0.5, 0, 1, 0, 0.75, 0, 1, 0, 0.75, 0, 1 ]; let vao1 = gl.createVertexArray(); prepare(gl, program, "position", 3, tri1, 0, 0, vao1); prepare(gl, program, "color", 4, tri1, 0, 4*9, vao1); let vao2= gl.createVertexArray(); prepare(gl, program, "position", 3, tri2, 0, 0, vao2); prepare(gl, program, "color", 4, tri2, 0, 4*9, vao2); // キャンバスいっぱいをクリップ空間とする gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // キャンバスクリア gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); draw(gl, program, vao1); draw(gl, program, vao2); } function draw(gl, program, vao) { // レンダリングに使用するシェーダープログラムを指定 gl.useProgram(program); // レンダリングするVAOを指定 gl.bindVertexArray(vao); // レンダリング let primitiveType = gl.TRIANGLES; let first = 0; let count = 3; gl.drawArrays(primitiveType, first, count); } function prepare(gl, program, varName, _size, data, _stride, _offset, vao) { // VBOの設定 let indexOfVbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, indexOfVbo); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW); // VAOの設定 let attributeLocation = gl.getAttribLocation(program, varName); gl.bindVertexArray(vao); // VBO & VAOの設定 let size = _size; let type = gl.FLOAT; let normalize = false; let stride = _stride; let offset = _offset; gl.vertexAttribPointer( attributeLocation, size, type, normalize, stride, offset ); gl.enableVertexAttribArray(attributeLocation); } main();
割といい感じなグラデーション画像になっていますね。
コード解説 - ユーザー定義関数 -
今回のコードで利用しているユーザー定義関数は次のようになっています。
戻り値の型 | 関数名 |
---|---|
WebGLShader | createShader() |
WebGLProgram | createProgram() |
なし | prepare() |
なし | draw() |
createShader()
function createShader(gl, type, source) { let shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); if (success) { return shader; } console.log(gl.getShaderInfoLog(shader)); // eslint-disable-line gl.deleteShader(shader); return undefined; }
この関数は文字列として定義されたシェーダーをコンパイルして利用可能な型(WebGLShader)としてリターンします。
createProgram()
function createProgram(gl, vertexShader, fragmentShader) { let program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); let success = gl.getProgramParameter(program, gl.LINK_STATUS); if (success) { return program; } console.log(gl.getProgramInfoLog(program)); // eslint-disable-line gl.deleteProgram(program); return undefined; }
この関数はコンパイル済みのシェーダー(WebGLShader)を受け取り最終的に利用可能な型(WebGLProgram)としてリターンします。
createShader()とcreateProgram()はWebGL2 Fundamentalsのサンプルコードを丸写ししました。
parepare()
function prepare(gl, program, varName, _size, data, _stride, _offset, vao) { // VBOの設定 let indexOfVbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, indexOfVbo); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW); // VAOの設定 let attributeLocation = gl.getAttribLocation(program, varName); gl.bindVertexArray(vao); // VBO & VAOの設定 let size = _size; let type = gl.FLOAT; let normalize = false; let stride = _stride; let offset = _offset; gl.vertexAttribPointer( attributeLocation, size, type, normalize, stride, offset ); gl.enableVertexAttribArray(attributeLocation); }
最も煩雑な処理を含んだ関数です。
// VBOの設定 let indexOfVbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, indexOfVbo); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
この三行でGPU上のメモリにデータが転送されます。注意点ですが、この時点ではまだ GPUは送られてきたデータが何を表しているのを知りません 。なので、このデータが具体的にどのようなデータなのかをGPUに教えてやる必要があります。このとき、生のデータ自体を管理するものを頂点バッファオブジェクト(VBO)といい、このVBOを管理するものを頂点配列オブジェクト(VAO)といいます。
// VAOの設定 let attributeLocation = gl.getAttribLocation(program, varName); gl.bindVertexArray(vao);
この二行はVAOを利用するための準備をしています。なお、今回のコードはVAOの生成はmain()内で行うようにしています。varNameにはシェーダー中の変数名です。例えば、gl.getAttribLocation(program, "position")とするとこのシェーダープログラム中のpositionという変数の管理番号(実際には0以上のインデックス)が得られます。
// VBO & VAOの設定 let size = _size; let type = gl.FLOAT; let normalize = false; let stride = _stride; let offset = _offset; gl.vertexAttribPointer( attributeLocation, size, type, normalize, stride, offset ); gl.enableVertexAttribArray(attributeLocation);
このコードで メモリ上のデータがどのようなデータなのかをGPUに覚えさせ、VAOを介すことでシェーダープログラムからその情報に参照できるようにします。もう少し具体的に言うと、gl.vertexAttribPointer()のおかげでシェーダープログラム中の変数とGPUのメモリ上の情報が結びつきます。
ありていに言うと、
といった感じでしょうか。
実は、1つの段ボールにすべての情報を含んでもよいですし、意味のある情報ごとに分けてもよいです。つまり、位置や色を一つのVBOに含めてもよいですし、逆に2つのVBOに分けても良いのです。
今回のコードは実は無駄なことをしていて、一つのVAOに対して位置と色の情報を含んだVBOを二回ずつ生成しています*2。本来であれば位置と色の情報を含んだVBOを一度だけ生成するか、位置の情報を含んだVBOと色の情報を含んだVBOを1度ずつ生成すれば十分なはずです。
無駄のないコードに直してみるのもいいですね。
draw()
function draw(gl, program, vao) { // レンダリングに使用するシェーダープログラムを指定 gl.useProgram(program); // レンダリングするVAOを指定 gl.bindVertexArray(vao); // レンダリング let primitiveType = gl.TRIANGLES; let first = 0; let count = 3; gl.drawArrays(primitiveType, first, count); }
実際に描画を行う関数です。今回のコードではインデックスを用いていないので与えられた3点を用いて三角形を描画します。
コード解説 - main関数 -
webgl2コンテクストを取得
function main() { // コンテクスト取得 let canvas = document.getElementById("canvas"); let gl = canvas.getContext("webgl2"); if (!gl) { return; } . . .
webgl2コンテクストを取得します。以後はOpenGLとほぼ同一の手続きを行います。
シェーダーのコンパイル、リンク
// バーテックスシェーダーとフラグメントシェーダーをコンパイル let vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); let fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // バーテックスシェーダーとフラグメントシェーダーをプログラムとリンクする let program = createProgram(gl, vertexShader, fragmentShader);
ユーザー定義関数を利用してシェーダーのコンパイルし、実際の描画に利用できるプログラムを生成します。
シェーダー中のin変数とプロクラム上のインデックスを結びつける
gl.bindAttribLocation(program, 0, "position"); gl.bindAttribLocation(program, 1, "color");
以降、programにとってattribute[0]と言えばposition、attribute[1]といえばcolorを表すようになります。
VBOに渡す情報を定義
const tri1 = [ // x, y, z 1, 1, 0, -1, -1, 0, 1, -1, 0, // r, g, b, a 0, 0.75, 0, 1, 0, 0.75, 0, 1, 0, 1, 0, 1 ]; const tri2 = [ // x, y, z, r -1, 1, 0, -1, -1, 0, 1, 1, 0, // r, g, b, a 0, 0.5, 0, 1, 0, 0.75, 0, 1, 0, 0.75, 0, 1 ];
そのまんまです。今回は[位置,位置,位置,色,色,色]という順番で情報が並んでいますが、prepareの引数を変えれば[位置,色,位置,色,位置,色]にも対応することが出来ます.
VAO、VBOの設定
let vao1 = gl.createVertexArray(); prepare(gl, program, "position", 3, tri1, 0, 0, vao1); prepare(gl, program, "color", 4, tri1, 0, 4*9, vao1); let vao2= gl.createVertexArray(); prepare(gl, program, "position", 3, tri2, 0, 0, vao2); prepare(gl, program, "color", 4, tri2, 0, 4*9, vao2);
詳しい説明はユーザー定義関数の解説のところを見てください。
描画
// キャンバスいっぱいをクリップ空間とする gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // キャンバスクリア gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); draw(gl, program, vao1); draw(gl, program, vao2);
VAOを指定した後描画を行います。
コード解説、以上!
おわりに
後半は力尽きて走り書き気味でしたが、基本的なところだけでもおさえられてよかったです。個人的にはWebGLの理解にはOpenGLの理解が重要だと思いました。OpenGLの教科書としては次の本に一番お世話になりました。
また、OpenGLを管理しているkhronosグループのクイックリファレンスも参考になりました。 https://www.khronos.org/files/webgl20-reference-guide.pdf
あとMDN Web Docsもお世話になりました。 developer.mozilla.org
今後の目標
- vertexAttribPointer()の引数のメモを書く
- インデックス情報を用いたオブジェクトの表現
- Canvasの画像化