はじめに
webGLで単色正方形を作る。簡単なシェーダーを作成し、400x400のキャンバスを単色で塗りつぶす。
コード全文
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>code0</title>
<style>
body{
margin:0;
}
main{
height:100dvh;
display:flex;
justify-content:center;
align-items:center;
}
</style>
</head>
<body>
<main>
<canvas id="canvas0"></canvas>
</main>
<script src="main.js"></script>
</body>
</html>
function loadSketch0(){
// 1. vsを作る
const vs =
`#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);
}
`;
// 2. fsを作る
const fs =
`#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(0.0, 0.5, 1.0, 1.0);
}
`;
// 3. コンテキスト
const cvs = document.getElementById("canvas0");
cvs.width = 400;
cvs.height = 400;
const gl = cvs.getContext('webgl2');
// 4.1. shader compile (vs)
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));
}
// 4.2. shader compile (fs)
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));
}
// 5. program
const pg = gl.createProgram();
// 6. attach and linking
gl.attachShader(pg, vsShader);
gl.attachShader(pg, fsShader);
gl.linkProgram(pg);
if(!gl.getProgramParameter(pg, gl.LINK_STATUS)){
console.log("programのlinkに失敗しました");
console.error(gl.getProgramInfoLog(program));
}
// 7. drawCall
gl.useProgram(pg);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
gl.flush();
}
document.addEventListener("DOMContentLoaded", loadSketch0);
実行結果
解説
htmlとcssについて
キャンバス要素をmainタグで挟んでいる。main要素の縦方向の長さはdvhの100%で画面いっぱい。display:flexを使うとキャンバスを簡単に中央に配置できる。キャンバスの大きさはjsサイドで決めている。
シェーダーについて
webGLではシェーダーと呼ばれる文字列を使って描画する。これらはC言語をベースとしたglslという言語で書かれる。バーテックスシェーダで頂点の位置を決めてドローコールを元にして三角形を生成、さらにフラグメントシェーダでそこに色を付ける。
#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);
}
今回はwebgl2でやるので最初にversion 300 esを追記する。webgl2ではこれを省いて旧webglで描画することも一応許されているが、特にメリットが無いのでwebgl2で作成する。もう対応してないデバイスもほぼ無いと思うので。あっても無視する。
定数の4つのvec2からなる配列を用意する。gl_VertexIDはドローコールに用いられる定数で、今回は0,1,2,3で描画するのでそれらが入る。つまり左下、右下、左上、右上の位置座標が入る。どこで描画してるかというと正規化デバイス座標系である。これは上下左右が±1で、x軸正方向が右、y軸正方向が上、高校でおなじみの座標系。
ドローコールはTRIANGLE_STRIPを用いている。
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
これは「0から4つの数字(0,1,2,3)でTRIANGLE_STRIPにより三角形を生成せよ」という命令で、その場合「0,1,2」と「2,1,3」で三角形が作られる。三角形の頂点の正の向きは反時計回りである(x軸を最短でy軸に重ねる向き)。だからイメージすると分かるが直角二等辺三角形が2枚できる。これで画面全体がおおわれる。
gl_Positionに4つの頂点の位置が入る。3番目の座標はとりあえず無視で。基本的に2次元ならここは常に0である。4つ目も通常は常に1である。
次にフラグメントシェーダで色を付ける。
#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(0.0, 0.5, 1.0, 1.0);
}
outで指定されるのはvec4の色のベクトルである。それを0,0.5,1,1の水色にしているので、そういう色で三角形が塗られることになる。これで出来ました。なおprecision highp floatの指定はフラグメントシェーダの場合必須で、これはシェーダー内で使う小数の精度を決めている。
シェーダーを作る
次にmain.jsでシェーダーを作るところ。
// 3. コンテキスト
const cvs = document.getElementById("canvas0");
cvs.width = 400;
cvs.height = 400;
const gl = cvs.getContext('webgl2');
// 4.1. shader compile (vs)
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));
}
// 4.2. shader compile (fs)
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));
}
まずキャンバスからwebgl2のコンテキストを取る。サイズは400x400で指定する。createShaderに引数を入れるとシェーダーオブジェクトが生成される。文字列をそのまま使うわけにはいかないので、入れ物が必要なんだね。ここに文字列を当てはめるのがshaderSourceという関数で、当てはめたらcompileShaderでシェーダーオブジェクトを完成させる。この際にエラーが出ないかどうかをgetShaderParameterで調べている。getShaderInfoLogで内容を知ることができる。
実際にやってみようか。
#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);
}
どこがまずいかわかりますか?エラーが出ます。
vertex shaderの作成に失敗しました main.js:36:13
ERROR: 0:7: 'gl_Position' : syntax error
main.js:37:13
loadSketch0 file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:37
(非同期: EventListener.handleEvent)
<匿名> file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:70
programのlinkに失敗しました main.js:59:13
Uncaught ReferenceError: program is not defined
loadSketch0 file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:60
EventListener.handleEvent* file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:70
main.js:60:40
loadSketch0 file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:60
(非同期: EventListener.handleEvent)
<匿名> file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:70
WebGL warning: linkProgram: Must have a compiled vertex shader attached:
SHADER_INFO_LOG:
ERROR: 0:7: 'gl_Position' : syntax error
答えはpos[gl_VertexID]の後のセミコロンの排除。これが無いとえらーをくらう。セミコロンは分の区切りなのでpythonなどと違って絶対に省略できない。自分がjs書いてるときセミコロンを欠かさないのは単に几帳面なのと、glslと違うルールで書くのが面倒だから。pythonでは書かないけど。それもpythonのルールに従ってるだけ。これをやるとgl_Positionが正しく認識されないのでエラーになる。
次に...
#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);
}
これでエラーが出る。
vertex shaderの作成に失敗しました main.js:36:13
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'
main.js:37:13
loadSketch0 file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:37
(非同期: EventListener.handleEvent)
<匿名> file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:70
programのlinkに失敗しました main.js:59:13
Uncaught ReferenceError: program is not defined
loadSketch0 file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:60
EventListener.handleEvent* file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:70
main.js:60:40
loadSketch0 file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:60
(非同期: EventListener.handleEvent)
<匿名> file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:70
WebGL warning: linkProgram: Must have a compiled vertex shader attached:
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なんて関数は知らんというわけである。もちろん直接定義すれば可能。glsl内で関数を定義する方法については割愛する。vec4とちゃんと書かないと駄目というわけ。
フラグメントシェーダのケース。
#version 300 es
out vec4 fragColor;
void main(){
fragColor = vec4(0.0, 0.5, 1.0, 1.0);
}
precision宣言をまるごとカット。結果はこちら:
fragment shaderの作成に失敗しました main.js:46:13
ERROR: 0:3: '' : No precision specified for (float)
main.js:47:13
loadSketch0 file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:47
(非同期: EventListener.handleEvent)
<匿名> file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:70
programのlinkに失敗しました main.js:59:13
Uncaught ReferenceError: program is not defined
loadSketch0 file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:60
EventListener.handleEvent* file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:70
main.js:60:40
loadSketch0 file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:60
(非同期: EventListener.handleEvent)
<匿名> file:///C:/Users/sea_g/OneDrive/ドキュメント/fisce.net/webGL/code/code0/main.js:70
WebGL warning: linkProgram: Must have a compiled fragment shader attached:
SHADER_INFO_LOG:
ERROR: 0:3: '' : No precision specified for (float)
precisionの宣言が無いことをダイレクトに注意される。floatについて宣言してください、と親切に回答をくれる。ありがたい。
プログラムの作成
シェーダーコンパイルに成功したら次はプログラムを作る。リンクという作業で、vsとfsをつなげて一つのプログラムを完成させる。
// 5. program
const pg = gl.createProgram();
// 6. attach and linking
gl.attachShader(pg, vsShader);
gl.attachShader(pg, fsShader);
gl.linkProgram(pg);
if(!gl.getProgramParameter(pg, gl.LINK_STATUS)){
console.log("programのlinkに失敗しました");
console.error(gl.getProgramInfoLog(program));
}
createProgramでプログラムオブジェクトを作り、attachShaderでシェーダーを当てはめる。順番は自由。シェーダーオブジェクトは自分がvsかfsかを知っているので機構が勝手に判断する。あとはlinkProgramするとそれらがつながれてプログラムが完成する。
この場合もエラーを調べることができてgetProgramParameterという。リンクステータスを調べるとリンクに失敗したかどうかを判定できる。getProgramInfoLogを使って内容を知れる。vsとfsの双方がコンパイルされていれば問題ないかというとそうでもないが、ここでは触れない。
ドローコール
プログラムができたのでuseProgramで起動させよう。
// 7. drawCall
gl.useProgram(pg);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
gl.flush();
今回はドローコール以外何もしないのでドローコールするだけである。さっき説明したTRIANGLE_STRIPと0と4で描画する。最後におまじないのflush. お疲れ様でした。
おわりに
おわりです。
gl定数について
gl.VERTEX_SHADERやgl.FRAGMENT_SHADERが出てきたが、これらは定数である。実は数で、console.logで値を調べることができる。しかし通常は数で運用することはないだろう。裏技的にその数そのものを利用したコードを書くこともあるにはある。コンテキストが無くてもアクセスしたい場合はそういう方法に頼るかもしれない。
console.log(gl.VERTEX_SHADER); // 35633
console.log(gl.FRAGMENT_SHADER); // 35632
fsの方が小さいのは意外ですね。
今回の登場人物
gl.createShader
リンク:gl.createShader構文:createShader(type)
引数:typeはgl.VERTEX_SHADERもしくはgl.FRAGMENT_SHADER
返値:WebGLShaderオブジェクト。
gl.shaderSource
リンク:gl.shaderSource構文:shaderSource(shader, source)
引数:shaderはWebGLShaderオブジェクト、sourceはシェーダー文字列。
返値:ありません