三角形を描画するスクリプト を通してWebGL2の基礎を理解しましょう。
はじめに
WebGL2とはざっくりいうとJavaScript からOpenGL の機能を利用することができるJavaScriptAPIです. ブラウザの機能なのでプラグイン などが必要なく、既存のOpenGL のコードをかなり流用できます*1 。
WebGL2があるからにはWebGL (無印)もあるわけですが、せっかくなら新しい方を使おうということでこの記事ではWebGL (無印)に関しては一切触れません。
また、WebGL2を「完全に理解」するためには広範な知識が必要になります。したがって、この記事でカバーする部分とカバーしない部分を明確にしておきましょう。
この記事で扱うこと
シェーダープログラムをコンパイル する方法
頂点バッファーオブジェクト(VBO)の利用方法
頂点配列オブジェクト(VAO)の利用方法
この記事で扱わないこと
GLSLの書き方
インデックスの頂点バッファオブジェクトの利用方法
Canvas を画像化してダウンロードする方法
vertexAttribPointer()の引数の詳しい説明
コードの全貌
いきなりですが、以下が完成したコードになります。
< head >
< style >
canvas {
width : 512px ;
height :512px ;
}
</ style >
</ head >
< body >
< canvas id = "canvas" ></ canvas >
< script src = "script.js" ></ script >
</ body >
"use strict" ;
let vertexShaderSource = `#version 300 es
in vec4 position;
in vec4 color;
out vec4 vcolor;
void main() {
vcolor = color;
gl_Position = position;
}
`;
let fragmentShaderSource = `#version 300 es
precision highp float ;
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));
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));
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 = [
1, 1, 0,
-1, -1, 0,
1, -1, 0,
0, 0.75, 0, 1,
0, 0.75, 0, 1,
0, 1, 0, 1
] ;
const tri2 = [
-1, 1, 0,
-1, -1, 0,
1, 1, 0,
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);
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)
{
let indexOfVbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, indexOfVbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
let attributeLocation = gl.getAttribLocation(program, varName);
gl.bindVertexArray(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));
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));
gl.deleteProgram(program);
return undefined ;
}
この関数はコンパイル 済みのシェーダー(WebGLShader)を受け取り最終的に利用可能な型(WebGLProgram)としてリターンします。
createShader()とcreateProgram()はWebGL2 Fundamentals のサンプルコードを丸写ししました。
webgl2fundamentals.org
parepare()
function prepare(gl, program, varName, _size, data, _stride, _offset, vao)
{
let indexOfVbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, indexOfVbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
let attributeLocation = gl.getAttribLocation(program, varName);
gl.bindVertexArray(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);
}
最も煩雑な処理を含んだ関数です。
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)といいます。
let attributeLocation = gl.getAttribLocation(program, varName);
gl.bindVertexArray(vao);
この二行はVAOを利用するための準備をしています。なお、今回のコードはVAOの生成はmain()内で行うようにしています。varNameにはシェーダー中の変数名です。例えば、gl.getAttribLocation(program, "position")とするとこのシェーダープログラム中のpositionという変数の管理番号(実際には0以上のインデックス)が得られます。
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 のメモリ上の情報が結びつきます。
ありていに言うと、
VBO→GPU 上のメモリにおくるデータを含んだ段ボール
VAO→各段ボールにどんな情報がどんな順番で入っているかを記録しておく伝票
といった感じでしょうか。
実は、1つの段ボールにすべての情報を含んでもよいですし、意味のある情報ごとに分けてもよいです。つまり、位置や色を一つのVBOに含めてもよいですし、逆に2つのVBOに分けても良いのです。
今回のコードは実は無駄なことをしていて、一つのVAOに対して位置と色の情報を含んだVBOを二回ずつ生成しています*2 。本来であれば位置と色の情報を含んだVBOを一度だけ生成するか、位置の情報を含んだVBOと色の情報を含んだVBOを1度ずつ生成すれば十分なはずです。
無駄のないコードに直してみるのもいいですね。
draw()
function draw(gl, program, vao)
{
gl.useProgram(program);
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 = [
1, 1, 0,
-1, -1, 0,
1, -1, 0,
0, 0.75, 0, 1,
0, 0.75, 0, 1,
0, 1, 0, 1
] ;
const tri2 = [
-1, 1, 0,
-1, -1, 0,
1, 1, 0,
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 の画像化