はじめに
このドキュメントの基礎となる Three.js の概要をまずお読みください。
シェーダーについて説明します。WebGL は素晴らしい技術です。前述のとおり、Three.js(およびその他のライブラリ)は、難しい部分を抽象化してくれます。しかし、特定の効果を実現したい場合や、画面に表示される素晴らしい効果についてもう少し詳しく知りたい場合、シェーダーはその方程式の一部になることはほぼ間違いありません。前回のチュートリアルの基本的な内容から、もう少し複雑な内容に進むことをおすすめします。
ここでは、Three.js を使用していることを前提とします。Three.js は、シェーダーの実行に関して多くの処理を行ってくれます。 このドキュメントでは、まずシェーダーのコンテキストについて説明し、その後、高度な手順について説明します。
1. 2 つのシェーダー
WebGL では固定パイプラインを使用できません。つまり、すぐに使えるレンダリング手段が提供されていません。代わりに、より強力ですが、理解と使用がより難しいプログラマブル パイプラインが提供されます。簡単に言うと、プログラマブル パイプラインとは、プログラマーが頂点などを画面にレンダリングする責任を負うことを意味します。シェーダーはこのパイプラインの一部であり、次の 2 種類があります。
- 頂点シェーダー
- フラグメント シェーダー
どちらも、それ自体では何の意味もありません。これらについて知っておくべきことは、どちらもグラフィック カードの GPU で完全に実行されるということです。つまり、可能な限り多くの処理を GPU にオフロードして、CPU が他の処理を行えるようにする必要があります。最新の GPU は、シェーダーが必要とする機能に高度に最適化されているため、シェーダーを使用できることは大きなメリットです。
2. 頂点シェーダー
球体などの標準的なプリミティブ シェイプを使用します。頂点で構成されていますよね。頂点シェーダーは、これらの頂点を 1 つずつ順番に受け取り、それらを操作できます。各頂点に対して実際に何を行うかは頂点シェーダー次第ですが、頂点シェーダーには 1 つの役割があります。それは、画面上の頂点の最終的な位置である 4D 浮動小数点ベクトル gl_Position を設定することです。これは、3D 位置(x、y、z の頂点)を 2D 画面に投影するという、非常に興味深いプロセスです。幸いなことに、Three.js のようなものを使用している場合は、処理が重くなりすぎずに gl_Position を設定する簡単な方法があります。
3. フラグメント シェーダー
頂点を持つオブジェクトがあり、それらを 2D 画面に投影しましたが、使用する色はどうでしょうか。テクスチャリングとライティングはどうでしょうか。フラグメント シェーダーは、まさにそのためにあります。頂点シェーダーと同様に、フラグメント シェーダーにも必須のジョブが 1 つだけあります。それは、gl_FragColor 変数を設定または破棄することです。これは、フラグメントの最終的な色である別の 4D 浮動小数点ベクトルです。
では、フラグメントとは何でしょうか。三角形を構成する 3 つの頂点を考えてみましょう。その三角形内の各ピクセルを描画する必要があります。フラグメントは、三角形の各ピクセルを描画するために、3 つの頂点によって提供されるデータです。このため、フラグメントは、構成する頂点から補間された値を受け取ります。1 つの頂点が赤色で、その隣が青色の場合、色の値は赤から紫、青へと補間されます。
4. シェーダー変数
作成できる変数宣言には、Uniforms、Attributes、Varyings の 3 つがあります。これらは他の一般的な言語と一致しないため、混乱を招く可能性があります。次のように考えるとわかりやすいでしょう。
ユニフォームは、頂点シェーダーとフラグメント シェーダーの両方に送信され、レンダリングされるフレーム全体で同じ値が保持されます。この良い例は、ライトの位置です。
属性は、個々の頂点に適用される値です。属性は頂点シェーダーでのみ使用できます。たとえば、各頂点に異なる色を割り当てることができます。属性は頂点と 1 対 1 の関係にあります。
Varying は、フラグメント シェーダーと共有する頂点シェーダーで宣言された変数です。これを行うには、頂点シェーダーとフラグメント シェーダーの両方で、同じ型と同じ名前の varying 変数を宣言します。この典型的な用途は、ライティング計算で使用できる頂点の法線です。
後で、3 つのタイプをすべて使用して、実際にどのように適用されるかを確認します。
いいえ。作成できる最もシンプルなシェーダーの 1 つを見てみましょう。
5. Bonjourno World
頂点シェーダーの Hello World は次のようになります。
/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
フラグメント シェーダーについても同様です。
/**
* Set the color to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}
それほど複雑ではないですよね?
頂点シェーダーでは、Three.js からいくつかの uniform が送信されます。これら 2 つの uniform は 4D 行列で、モデルビュー行列と射影行列と呼ばれます。これらの仕組みを正確に把握する必要はありませんが、可能であれば、仕組みを理解することをおすすめします。簡単に言うと、頂点の 3D 位置を画面上の最終的な 2D 位置に実際に投影する方法です。
Three.js がシェーダー コードの先頭に自動的に追加するため、自分で追加する必要はありません。実際には、ライトデータ、頂点カラー、頂点法線など、さらに多くのデータが追加されます。Three.js を使用せずにこれを行う場合は、これらのユニフォームと属性をすべて自分で作成して設定する必要があります。実話です。
6. MeshShaderMaterial を使用する
シェーダーを設定しましたが、Three.js でどのように使用すればよいでしょうか。
/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader: $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});
そこから、Three.js は、そのマテリアルを割り当てたメッシュにアタッチされたシェーダーをコンパイルして実行します。これほど簡単なことはありません。おそらくそうでしょう。しかし、ブラウザで 3D を実行することについて話しているので、ある程度の複雑さは予想されると思います。
MeshShaderMaterial には、uniforms と attributes という 2 つのプロパティを追加できます。どちらもベクトル、整数、浮動小数点数を取ることができますが、前述のように、uniform はフレーム全体またはすべての頂点に対して同じであるため、単一の値になる傾向があります。ただし、属性は頂点ごとの変数であるため、配列であることが想定されます。属性配列の値の数とメッシュの頂点の数の間には、1 対 1 の関係が必要です。
7. 次のステップ
次に、アニメーション ループ、頂点属性、ユニフォームを追加します。また、頂点シェーダーがフラグメント シェーダーにデータを送信できるように、varying 変数も追加します。最終的な結果として、ピンク色の球体が上部と側面から照らされているように見え、脈動します。少し複雑ですが、3 つの変数タイプと、それらの相互関係、基盤となるジオメトリについて理解を深めることができます。
8. フェイクライト
次に、単色のオブジェクトにならないように色を更新します。Three.js がライティングをどのように処理するかを確認することもできますが、おわかりのように、これは必要以上に複雑なため、ここではライティングを模倣することにします。Three.js の一部である素晴らしいシェーダーと、Chris Milk と Google による素晴らしい WebGL プロジェクト Rome のシェーダーをぜひご覧ください。シェーダーの話に戻りましょう。頂点シェーダーを更新して、各頂点の法線をフラグメント シェーダーに渡します。このために、以下の要素を変化させます。
// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;
void main() {
// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
フラグメント シェーダーでは、同じ変数名を設定し、頂点法線と、球の上部右側から照射される光を表すベクトルの内積を使用します。この結果、3D パッケージの指向性ライトと同様の効果が得られます。
// same name and type as VS
varying vec3 vNormal;
void main() {
// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
// ensure it's normalized
light = normalize(light);
// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));
// feed into our frag color
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);
}
ドット積が機能する理由は、2 つのベクトルが与えられたときに、2 つのベクトルがどれほど「類似」しているかを示す数値が得られるからです。正規化されたベクトルの場合、まったく同じ方向を指していると、値は 1 になります。反対方向を向いている場合は -1 になります。この数値を照明に適用します。したがって、右上の頂点の値は 1(完全に照らされている)に近く、側面の頂点の値は 0 に近く、裏面の頂点の値は -1 になります。負の値は 0 にクランプしますが、数値を代入すると、基本的なライティングが得られます。
次のステップ: 頂点の位置を調整してみると 面白いかもしれません
9. 属性
ここで、属性を使用して各頂点に乱数を割り当てます。この数値を使用して、頂点を法線に沿って押し出します。最終的な結果は、ページを更新するたびに変化する、奇妙なスパイクボールのようなものになります。まだアニメーション化はされませんが(次のステップで行います)、ページを数回更新すると、ランダム化されていることがわかります。
まず、頂点シェーダーに属性を追加します。
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position +
normal *
vec3(displacement);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
実際の画面
実際にはあまり違いはありません。これは、属性が MeshShaderMaterial で設定されていないため、シェーダーが代わりにゼロ値を使用するためです。現時点ではプレースホルダのようなものです。この後、JavaScript で MeshShaderMaterial に属性を追加すると、Three.js が自動的に 2 つを結び付けます。
また、更新された位置を新しい vec3 変数に割り当てる必要があったことも注目に値します。これは、元の属性が、すべての属性と同様に読み取り専用であるためです。
10. MeshShaderMaterial を更新する
ディスプレイスメントを強化するために必要な属性を使用して、MeshShaderMaterial の更新に直接進みます。属性は頂点ごとの値であるため、球の頂点ごとに 1 つの値が必要です。この場合、次のように指定します。
var attributes = {
displacement: {
type: 'f', // a float
value: [] // an empty array
}
};
// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}
球体が変形していますが、興味深いのは、すべての変位が GPU で行われていることです。
11. アニメーションにする
この要素をアニメーション化する必要があります。それをどのように行っていますか?次の 2 つの準備が必要です。
- 各フレームに適用するディスプレイスメントの量をアニメーション化する uniform。正弦または余弦は -1 から 1 の範囲で変化するため、これらを使用できます。
- JS のアニメーション ループ
MeshShaderMaterial と頂点シェーダーの両方に uniform を追加します。まず、頂点シェーダーです。
uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position +
normal *
vec3(displacement *
amplitude);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
次に、MeshShaderMaterial を更新します。
// add a uniform for the amplitude
var uniforms = {
amplitude: {
type: 'f', // a float
value: 0
}
};
// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms: uniforms,
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
シェーダーの作成はこれで完了です。しかし、現時点では一歩後退したように見えます。これは主に、振幅値が 0 であり、それを変位に乗算しているため、何も変化していないように見えるためです。また、アニメーション ループを設定していないため、0 が他の値に変化することはありません。
JavaScript では、レンダリング呼び出しを関数にラップし、requestAnimationFrame を使用して呼び出す必要があります。そこで、ユニフォームの値を更新する必要もあります。
var frame = 0;
function update() {
// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;
renderer.render(scene, camera);
// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);
まとめ
これで完了です。これで、奇妙な(そして少しサイケデリックな)脈動するアニメーションが表示されるようになりました。
シェーダーについては、他にもさまざまなトピックがありますが、この入門編がお役に立てば幸いです。これで、シェーダーを理解し、独自の素晴らしいシェーダーを作成できるようになりました。