WebGL総本山やWebGLの開発支援サイトwgld.orgを運営している、杉本 雅広さんによるGLSLの講義を受けてきました。(全4回)
遅ればせながら、今回は第1回のまとめ。
周りを見渡すと、自分のようなひよっこエンジニアは見当たらず、オーラ全開ゴリゴリエンジニアといった感じで、「自分には早すぎたか…!?」と焦りました…;;
第1回は座学が多かったですが(むしろほぼ座学…?)内容としてはとても大事なことでした。
受講内容は、基本的に事前に受講者に配布されていたサンプルを見ながら進めていくような感じなので、まとめノートにするのはなかなか難しいところがありました。
以下、講義のまとめノートです↓↓↓
GLSLとはなにかを知ろう 〜シェーダとGLSLの概念・基礎知識〜
進化するシェーダ
- シェーダは高い演算能力をもつGPUを使用するため、映像表現だけではなく様々な演算処理に使われるように。(ブラウザ上で大量の演算をする際、裏でWebGLに関連した技術を使ったりなど)
- とはいえ、メインはやはりグラフィックスの描画がほとんど。
3D APIとシェーダ
- 当たり前のことながら、3DCGを描画するためには大量の計算や工程が必要になる。そのため、大抵はGPUを使用する。
- GPUを直接触るのは難しいため、インターフェースとして3D APIを利用する。
- 3D APIで有名なところは、オープンソースの
OpenGL
やWindowsに最適化されたDirectX
など。 - 【 CPU (JavaScriptなど) 】 => 【 3D API 】 => 【 GPU 】
OpenGL と OpenGL ES
-
OpenGL ES
はモバイル向けで軽量。
OpenGL ES と WebGL
- WebGLはJavaScriptのAPI。内部的には
OpenGL ES 2.0
とほぼ同じ。
GLSL
-
OpenGL Shading Language
の略。 - OpenGLファミリーで利用できるシェーダ記述言語。
- 現在のWebGL(WebGL 1.0)で利用するGLSLは
GLSL ES 1.0
。(GLSL ES 1.0は、OpenGL ES 2.0と利用されているものと同じ)
固定機能パイプラインとプログラマブルシェーダ
- かつてのOpenGLには、現在のシェーダの機能がすべて備わっており、すべてOpenGL単体で完結するように作られていた。(他の3D APIも同様)
- それでは汎用性に乏しく、ある程度同じことしかできなかったため、GLSLのような自由にシェーディングを制御できる機構が生まれた。
-
固定機能パイプライン
:OpenGL内に組み込まれていた、現在のシェーダに相当する機能。 -
プログラマブルシェーダ
:自由にシェーディングを制御できる機構。 - WebGLやOpenGL ESには
固定機能パイプライン
が存在しないため、プログラマブルシェーダ
を利用する必要がある。
WebGLで利用できるシェーダ
-
頂点シェーダ
- 頂点の座標変換が主な役割。
- 3Dプログラミングでは頂点によってあらゆるものが表現される。
- 頂点が1つで点、頂点が2つで線、頂点が3つで三角形…
頂点の個数 = 頂点シェーダが実行される回数
-
フラグメントシェーダ
- ピクセルシェーダとも呼ばれる。
- 最終的にスクリーンに描画されるピクセルひとつひとつを処理する。
- 頂点によって描画されることになるピクセルひとつひとつがフラグメントシェーダの処理対象になる。
画面の解像度の大きさ = 実行されるピクセルの個数
その他のシェーダ
- 代表的なものでは、ジオメトリシェーダ。(WebGLでは使えない)
- プラットフォームを限定するものもある。
- シェーダの中でも、
頂点シェーダ
とフラグメントシェーダ
は最も基本的なシェーダであり、非常に重要。
グラフィックスパイプライン
- ディスプレイに映像が映し出されるまでは、主にGPU側ではざっくりと以下のようになっている。
- 【 3Dモデルデータ (配列など) 】 => 【 頂点シェーダ (座標変換など) 】 => 【 ビューポート変換 】 => 【 ラスタライズ 】 => 【 フラグメントシェーダ (着色など) 】 => 【 ブレンディングなど 】 => 【 ディスプレイ 】
- この処理の流れをグラフィックスパイプラインと呼ぶ。(レンダリングパイプラインと呼ぶ場合も)
- かつては固定機能シェーダが埋め込まれており固定されていた(
固定機能パイプライン
)。頂点情報などを流し込むとレンダリングまで自動的に実行されていた。 - グラフィックスパイプラインのいくつかの工程を自由に実装できるようにしたものが
プログラマブルシェーダ
。 - 自由に実装できるといっても、処理の順序はある程度決まっている。(=シェーダが実行される順序も決まっている)
- 頂点シェーダがどう描画すべきかを確定させ、処理すべきピクセルに対してフラグメントシェーダが実行される。
- 上記から、常に
フラグメントシェーダより先に頂点シェーダが呼び出される
。
サンプルを見ながら考えていこう
- ここから、事前に受講者に配布されていたサンプル001のコードを見ながら、WebGL側の処理の流れを把握する。
WebGLの初期化の流れ
- とにかく工程が多い!(それはどの3D APIでも同じ)
-
GPUで動作するGLSL
とCPU側のロジック
が連携するために必要な手順を確認していく。 - CPU側であるJavaScriptやC++が、OpenGLやWebGLなどの3D APIをインターフェースとして、GPUで描画処理をおこなう。
- JavaScript側での処理についての流れ
- canvas要素からWebGL描画用のコンテキストを取得する。これを通して様々な処理をおこなう。
- GLSLで書かれたシェーダのソースコードを取得してコンパイルする。
- GLSLのソースコードは
単なる文字列
でJavaScript側でコンパイルするため、極論、変数に代入した文字列でもよいし、外部ファイルにするならば拡張子はなんでもよい。 - シェーダはコンパイルされることで、単なる文字列から、実態のある
シェーダオブジェクト
になる。 - 頂点シェーダとフラグメントシェーダという異なるシェーダを、それぞれデータの受け渡しができるようにするため
プログラムオブジェクト
にリンクさせる。 - この
プログラムオブジェクト
は、シェーダ同士やシェーダとCPUとやりとりをおこなってくれる、いわば管制塔のようなもの。 - これでシェーダを使った描画をおこなうための準備が完了する。
WebGLの描画までの流れ
-
プログラムオブジェクト
を通してGLSL側の変数の情報(変数名やデータ型など)をアプリケーション側(JavaScript側)で取得する。 - CPUとGPUが物理的に離れている関係上、対象の変数がGPU内のメモリの中のどの
ロケーション
を示しているかを取得する必要がある。 -
ロケーション
はプログラムオブジェクト
を通して取得することができる。これでCPU側とGPU側がつながったことになる。 - JavaScriptが動作するのはCPU側であり、シェーダが動作するのはGPU側であるため、GPU側の管制塔である
プログラムオブジェクト
が橋渡し役となるイメージ。 - CPUからは
ロケーション
を指定してデータを流し込む。
GLSLの変数
- GLSLの変数宣言の際に指定するもの
- 変数名
- 変数のデータ型
- int(整数)、float(浮動小数点)、bool(真偽値)、vec系(ベクトル。vec2〜vec4。中身はfloat)、mat系(行列。mat2〜mat4。中身はfloat)など。
- 変数の種類をあらわす修飾子(ストレージ修飾子)
-
attribute
:頂点の情報が格納される変数に用いられる。それぞれの頂点が固有に持っている座標位置や色などの情報。(頂点に固有の情報を一般に「頂点属性」という) -
uniform
:アプリケーション(JavaScript)から渡されるグローバル変数。どの頂点に対しても均一に作用する。全体に影響するパラメータなどを制御できる。 -
varying
:頂点シェーダからフラグメントシェーダの値の受け渡しに使われる。シェーダ間のやり取り用。
-
ロケーション取得方法の基本
-
attribute修飾子
とuniform修飾子
を持つ変数は、変数名
とまったく同じ文字列をプログラムオブジェクト
経由で検索することで、変数のロケーション
を取得できる。 - 描画が開始されるまでの間に事前にシェーダにデータを送るため、
ロケーション
をあらかじめ取得しておく必要がある。 - 取得した
ロケーション
の使い方は、attribute変数
とuniform変数
で手順が若干異なる。
frag.glsl
precision mediump float;
uniform vec4 globalColor;
void main() {
gl_FragColor = globalColor;
}
javascript
// GLSL側の変数 globalColor のロケーションを取得している。
// gl:WebGLコンテキスト、program:プログラムオブジェクト
gl.getUniformLocation(program, 'globalColor');
ロケーション取得から描画まで
-
ロケーション
の取得までできたら、残るはデータをシェーダに送り込んで描画をおこなうだけ。 - 手順はざっくりと以下のようになっている。
頂点の情報を定義
- 頂点は単純に配列として定義すればよい。GLSL側でvec3なら3個1セット、vec2なら2個1セット、floatなら単体の値。
- 以下の例は、GLSL側で
vec3
で宣言されたattribute変数 position
を渡すデータのため、JavaScript側でも、xyzでデータがワンセットになるように要素数を調整する。
vert.glsl
attribute vec3 position;
void main(){
gl_Position = vec4(position, 1.0);
gl_PointSize = 16.0;
}
javascript
// 頂点座標の定義(シェーダ内で参照する attribute変数の元になる最初のデータ定義)
position = [
0.0, 0.0, 0.0, // 1つ目の頂点の xyz
-0.5, 0.5, 0.0, // 2つ目の頂点の xyz
0.5, 0.5, 0.0, // 3つ目の頂点の xyz
-0.5, -0.5, 0.0, // 4つ目の頂点の xyz
0.5, -0.5, 0.0, // 5つ目の頂点の xyz
];
背景をクリアする色の指定
- WebGLやGLSLではRGBAは0.0〜1.0の範囲で指定する。
- GLSLでは
vec4(1.0, 1.0, 1.0, 1.0)
が完全に不透明な白(rgba(255, 255, 255, 1))に値する。 - JavaScript側で背景をクリアする色の指定をおこなうのも同様の値の範囲で指定する。
ビューポートが描画される矩形の定義
- WebGLやOpenGLでは、描画する領域をビューポートと呼ぶ。
- ウィンドウサイズやレンダリングの対象となっているcanvas要素の大きさが変化するときには、ビューポートの再設定ももちろん必要になる。(canvas要素の大きさに合わせるのが良さそう)
- ビューポートの再設定をしないと、canvas要素とビューポートのサイズに差が生じ、表示位置がずれて見えたり大きさが違って見えてしまう。
どのプログラムオブジェクトを利用するかしっかり指定
- うっかり別の
プログラムオブジェクト
に変えてしまうとシェーダがそっくり入れ替わることになる。 - 複数のシェーダを同時に切り替えながら使う場合は、描画前にしっかりと対象となる
プログラムオブジェクト
を選択しなくてはならない。
GPUにデータを送り、描画命令を発行する
- WebGLやOpenGLでは、描画を実際におこなうのはGPUであるため、描画をおこなうために必要なデータは、事前にGPUに渡しておく必要がある。
- データを送ってから描画命令を発行すると、その時点でGPUに送られている情報を使ってレンダリングされる。
-
VBO
とattributeロケーション
を使って頂点を有効にする =>uniformロケーション
を使って、uniform変数
にデータを転送する => 転送済みの情報を使って、頂点を画面にレンダリングする-
VBO
とは、Vertex Berffer Object
の頭文字をとったもの。シェーダに渡す値を詰める専用オブジェクトのようなもの。
-
GLSL側で gl_ の接頭語が付いているものはビルトイン変数
-
ビルトイン変数
:定義しなくてもそのまま利用可能な変数。 - GLSLで定義されている
ビルトイン変数
には、gl_
の接頭語がついている。以下は一例。 - 頂点シェーダ
-
gl_Position
:頂点シェーダから出力する頂点情報。 -
gl_PointSize
:頂点を点として描画するときのサイズ。(ピクセル単位)
-
- フラグメントシェーダ
-
gl_FragColor
:フラグメントシェーダから出力する色情報。(RGBAのvec4)
-
頂点属性に頂点カラーを追加する(サンプル002)
- 頂点カラーを頂点シェーダで受け取り、varying変数を使って、頂点の色をフラグメントシェーダへ渡す。
-
uniform変数
とattribute変数
とvarying変数
の違いをしっかり意識する。
javascript
// 〜 中略 〜
// 頂点座標の定義
position = [
0.0, 0.0, 0.0,
-0.5, 0.5, 0.0,
0.5, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
];
// 頂点カラーの定義(頂点シェーダの変数colorはvec4だから4つずつ)
color = [
1.0, 1.0, 1.0, 1.0, // 1つ目の頂点の色
1.0, 0.0, 0.0, 1.0, // 2つ目の頂点の色
0.0, 1.0, 0.0, 1.0, // 3つ目の頂点の色
0.0, 0.0, 1.0, 1.0, // 4つ目の頂点の色
0.5, 0.5, 0.5, 1.0, // 5つ目の頂点の色
];
// 〜 中略 〜
// グローバルカラー
gColor = [1.0, 1.0, 1.0, 1.0];
// 〜 中略 〜
vert.glsl
attribute vec3 position;
attribute vec4 color; // 頂点カラー
varying vec4 vColor; // varying変数でフラグメントシェーダへ値を送る
void main(){
// フラグメントシェーダへ送る値を、varying変数へ代入しておく
vColor = color;
gl_Position = vec4(position, 1.0);
gl_PointSize = 16.0;
}
frag.glsl
precision mediump float;
uniform vec4 globalColor;
varying vec4 vColor; // varying変数間で変数名は同一でなければならない
void main(){
gl_FragColor = globalColor * vColor;
}
頂点にポイントサイズの頂点属性を追加する(サンプル003)
- floatの頂点属性なので、ストライドが1になる。
- 頂点を描画する形式をプリミティブと呼ぶ。
- サンプルで使われている
gl.POINTS
のほか、gl.LINES
やgl.TRIANGLES
など。
- サンプルで使われている
javascript
// 〜 中略 〜
// 頂点座標の定義
position = [
0.0, 0.0, 0.0,
-0.5, 0.5, 0.0,
0.5, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
];
// 頂点カラーの定義
color = [
1.0, 1.0, 1.0, 1.0,
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.5, 0.5, 0.5, 1.0,
];
// 頂点のポイントサイズの定義
size = [2.0, 4.0, 8.0, 16.0, 32.0];
// 〜 中略 〜
// グローバルカラー
gColor = [1.0, 1.0, 1.0, 1.0];
// 〜 中略 〜
// 転送済みの情報を使って、頂点を画面にレンダリングする
// gl:WebGLコンテキスト、position:頂点座標(配列データ、glslではvec3で定義)
gl.drawArrays(gl.POINTS, 0, position.length / 3); // gl.POINTS:点(矩形)として描く
// 〜 中略 〜
vert.glsl
attribute vec3 position;
attribute vec4 color;
attribute float size; // ポイントサイズの頂点属性を追加
varying vec4 vColor;
void main(){
vColor = color;
gl_Position = vec4(position, 1.0);
gl_PointSize = size;
}
frag.glsl
precision mediump float;
uniform vec4 globalColor;
varying vec4 vColor;
void main(){
gl_FragColor = globalColor * vColor;
}
カーソルに連動して動くサンプルを作ってみる(サンプル004)
- カーソルの動きに合わせてすべての頂点が一律に動くので、uniform変数を利用する。
- カーソルの位置は
vec2
として扱う。
javascript
// 〜 中略 〜
// マウスカーソルが動いたことを検出するためのイベントを記述
mouseX = 0;
mouseY = 0;
window.addEventListener('mousemove', (e) => {
let x = e.clientX;
let y = e.clientY;
let width = window.innerWidth;
let height = window.innerHeight;
x = (x - width / 2.0) / (width / 2.0);
y = (y - height / 2.0) / (height / 2.0);
mouseX = x;
mouseY = -y;
});
// 頂点座標の定義
position = [
0.0, 0.0, 0.0,
-0.5, 0.5, 0.0,
0.5, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
];
// 頂点カラーの定義
color = [
1.0, 1.0, 1.0, 1.0,
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.5, 0.5, 0.5, 1.0,
];
// 頂点のポイントサイズの定義
size = [2.0, 4.0, 8.0, 16.0, 32.0];
// 〜 中略 〜
// グローバルカラー
gColor = [1.0, 1.0, 1.0, 1.0];
// 〜 中略 〜
vert.glsl
attribute vec3 position;
attribute vec4 color;
attribute float size;
uniform vec2 mouse; // マウスカーソルの位置(正規化済み)
varying vec4 vColor;
void main(){
vColor = color;
// マウスカーソルの動きを頂点座標にそのまま加算する
gl_Position = vec4(position + vec3(mouse, 0.0), 1.0);
gl_PointSize = size;
}
frag.glsl
precision mediump float;
uniform vec4 globalColor;
varying vec4 vColor;
void main(){
gl_FragColor = globalColor * vColor;
}
ウィンドウサイズに応じた変形(サンプル005)
-
gl_Position
に出力する頂点の情報は常に-1.0〜1.0
の範囲で扱われる。(正規化デバイス座標) - ウィンドウサイズに応じて歪まないようにするためには、
-1.0〜1.0
の空間に収まるよう頂点を変換する。
javascript
// 〜 中略 〜
// マウスカーソルが動いたことを検出するためのイベントを記述
mouseX = 0;
mouseY = 0;
window.addEventListener('mousemove', (e) => {
let x = e.clientX;
let y = e.clientY;
let width = window.innerWidth;
let height = window.innerHeight;
x = (x - width / 2.0) / (width / 2.0);
y = (y - height / 2.0) / (height / 2.0);
mouseX = x;
mouseY = -y;
});
// 頂点座標の定義
position = [
0.0, 0.0, 0.0,
-0.5, 0.5, 0.0,
0.5, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
];
// 頂点カラーの定義
color = [
1.0, 1.0, 1.0, 1.0,
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.5, 0.5, 0.5, 1.0,
];
// 頂点のポイントサイズの定義
size = [2.0, 4.0, 8.0, 16.0, 32.0];
// 〜 中略 〜
// グローバルカラー
gColor = [1.0, 1.0, 1.0, 1.0];
// 〜 中略 〜
vert.glsl
attribute vec3 position;
attribute vec4 color;
attribute float size;
uniform vec2 mouse;
uniform vec2 resolution; // 解像度
varying vec4 vColor;
void main(){
vColor = color;
// 解像度の情報からアスペクト比を計算して頂点を変換する
// 例)横2:縦1のaspectは0.5
float aspect = 1.0 / (resolution.x / resolution.y);
vec3 p = position * vec3(aspect, 1.0, 1.0);
gl_Position = vec4(p + vec3(mouse, 0.0), 1.0);
gl_PointSize = size;
}
frag.glsl
precision mediump float;
uniform vec4 globalColor;
varying vec4 vColor;
void main(){
gl_FragColor = globalColor * vColor;
}
やってみよう 水平に並べる(サンプル006)
- 水平に左から右まで等間隔に頂点を並べる。
vert.glsl
attribute vec3 position;
attribute float ratio; // 取りうる値は、0.0 ~ 1.0
void main(){
// ratio を -1.0 〜 1.0 に変換して、X座標に入れればよい
float x = ratio * 2.0 - 1.0;
gl_Position = vec4(position + vec3(x, 0.0, 0.0), 1.0);
gl_PointSize = 8.0;
}
やってみよう サイン波で揺らす(サンプル007)
- 水平に並べた後、サイン波で揺らしてみる。
- uniform変数を使って時間の経過(秒単位)が送られてくる。
vert.glsl
attribute vec3 position;
attribute float ratio; // 0.0 ~ 1.0
uniform float time; // 経過時間
void main(){
float x = ratio * 2.0 - 1.0;
float y = sin(x + time) * 0.5; // 0.5 => 波の高さの調整
gl_Position = vec4(position + vec3(x, y, 0.0), 1.0);
gl_PointSize = 8.0;
}
やってみよう 円形に配置する(サンプル008)
- サインとコサインを組み合わせて円を作る。
vert.glsl
attribute vec3 position;
attribute float ratio; // 0.0 ~ 1.0
uniform float time; // 経過時間
const float PI = 3.1415926; // 円周率
void main(){
float radian = ratio * 2.0 * PI;
float x = cos(radian);
float y = sin(radian);
gl_Position = vec4(position + vec3(x, y, 0.0), 1.0);
gl_PointSize = 8.0;
}
最後に
誤字・脱字などありましたら、すみません…