はじめに
以前、Three.js の概要について説明しました。まだ読んでいない場合は、この記事で構築する基盤となるため、ぜひ読んでください。
ここではシェーダーについて説明します。WebGL は優れた技術です。前述のように、Three.js(および他のライブラリ)は、複雑な処理を抽象化することで、開発者の負担を軽減します。ただし、特定の効果を実現したい場合や、画面に表示される素晴らしい効果を詳しく調べたい場合は、シェーダーがその計算の一部になる可能性があります。また、私と同じように、前回のチュートリアルの基本的な内容から、もう少し複雑なものに進みたいと思うかもしれません。シェーダーの開始に関して、面倒な作業の多くを Three.js が行うため、ここでは Three.js を使用していることを前提とします。最初にシェーダーのコンテキストについて説明します。このチュートリアルの後半では、少し高度な内容について説明します。これは、シェーダーが一見すると珍しく、説明が少し必要になるためです。
1. 2 つのシェーダー
WebGL では固定パイプラインの使用が提供されていません。つまり、すぐにレンダリングできる手段が提供されていないということです。ただし、プログラマブル パイプラインは、より強力ですが、理解と使用がより困難です。簡単に言うと、プログラマブル パイプラインは、プログラマが頂点などを画面にレンダリングする責任を負うことを意味します。シェーダーはこのパイプラインの一部であり、次の 2 種類があります。
- 頂点シェーダー
- フラグメント シェーダー
どちらも、それだけでは何の意味も持ちません。どちらも、グラフィック カードの GPU で完全に実行されます。つまり、可能な限りすべてを GPU にオフロードし、CPU に他の処理を任せたいということです。最新の GPU はシェーダーに必要な機能に対して高度に最適化されているため、使用できると便利です。
2. 頂点シェーダー
球などの標準プリミティブ シェイプを用意します。頂点で構成されていますよね。頂点シェーダーには、これらの頂点が 1 つずつ順番に渡され、頂点の処理を行うことができます。各頂点に対して実際に何を行うかは頂点シェーダー次第ですが、1 つの責任があります。それは、ある時点で gl_Position と呼ばれる 4D 浮動小数点ベクトルを設定することです。これは、画面上の頂点の最終的な位置です。これはそれ自体が非常に興味深いプロセスです。実際には、3D 位置(x、y、z を持つ頂点)を 2D 画面に取得または投影しています。幸い、Three.js などのライブラリを使用している場合は、負荷を増やすことなく gl_Position を設定するための簡単な方法があります。
3. フラグメント シェーダー
頂点を持つオブジェクトを作成して 2D 画面に投影しましたが、使用する色はどうすればよいでしょうか。テクスチャリングと照明はどうですか?フラグメント シェーダーはまさにそのためのものです。頂点シェーダーと非常によく似て、フラグメント シェーダーも必須の処理が 1 つだけあります。それは、フラグメントの最終的な色である別の 4D 浮動小数点ベクトルである gl_FragColor 変数を設定または破棄することです。では、フラグメントとは何でしょうか。三角形を構成する 3 つの頂点について考えてみましょう。その三角形内の各ピクセルを描画する必要があります。フラグメントは、その三角形の各ピクセルを描画するために、3 つの頂点から提供されるデータです。このため、フラグメントは構成頂点から補間された値を受け取ります。1 つの頂点が赤色で、隣接する頂点が青色の場合、色値は赤色から紫色を経て青色に補間されます。
4. シェーダー変数
変数には、ユニフォーム、属性、変化の 3 つの宣言があります。これらの 3 つを初めて聞いたとき、これまで扱ってきたものとは異なるため、非常に混乱しました。ただし、次のように考えることができます。
ユニフォームは、頂点シェーダーとフラグメント シェーダーの両方に送信され、レンダリングされるフレーム全体で同じ値が保持されます。たとえば、照明の位置がこれに該当します。
属性は、個々の頂点に適用される値です。属性は頂点シェーダーでのみ使用できます。たとえば、各頂点に異なる色を付けることができます。属性は頂点と 1 対 1 の関係にあります。
変化量は、頂点シェーダーで宣言され、フラグメント シェーダーと共有する変数です。これを行うには、頂点シェーダーとフラグメント シェーダーの両方で、同じ型と名前の可変変数を宣言します。古典的な用途は、照明計算で使用できる頂点の法線です。
後ほど、3 つのタイプすべてを使用して、実際に適用される方法を把握します。
ここまで、頂点シェーダーとフラグメント シェーダー、およびそれらが扱う変数の型について説明しました。次に、作成できる最もシンプルなシェーダーを見てみましょう。
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 colour 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 からいくつかのユニフォームが送信されます。これらの 2 つのユニフォームは、モデルビュー行列と投影行列と呼ばれる 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 にユニフォームと属性という 2 つのプロパティを追加できます。どちらもベクトル、整数、浮動小数点数を指定できますが、前述のようにユニフォームはフレーム全体(すべての頂点)で同じであるため、単一の値になる傾向があります。ただし、属性は頂点ごとの変数であるため、配列であることが期待されます。属性配列の値の数とメッシュの頂点の数の間には 1 対 1 の関係が必要です。
7. 次のステップ
次に、アニメーション ループ、頂点属性、ユニフォームを追加します。また、頂点シェーダーがフラグメント シェーダーにデータを送信できるように、可変変数も追加します。最終的な結果は、ピンク色だった球体が上部と側面から照らされ、脈動するように見えることです。少し複雑ですが、3 つの変数型と、それらが相互にどのように関連し、基盤となるジオメトリとどのように関連しているかを理解するのに役立つことを願っています。
8. 偽の光
フラットな色のオブジェクトにならないように、色付けを更新しましょう。Three.js が照明を処理する方法を確認することもできますが、現在必要なものよりも複雑であるため、フェイクで作成します。Three.js に含まれる素晴らしいシェーダーと、Chris Milk と Google による最近の素晴らしい WebGL プロジェクト Rome のシェーダーをすべて確認してください。シェーダーに戻りましょう。各頂点の法線をフラグメント シェーダーに提供するように、頂点シェーダーを更新します。そのため、Google は次のようなさまざまな方法でユーザーのプライバシーを保護しています。
// 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 colour
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);
}
ドット積が機能する理由は、2 つのベクトルを与えると、2 つのベクトルの「類似性」を示す数値が得られるからです。正規化されたベクトルがまったく同じ方向を向いている場合、値は 1 になります。反対方向を向いている場合は -1 になります。Google は、その数値を照明に適用します。そのため、右上の頂点の値は 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 つのことを準備する必要があります。
- 各フレームで適用する変位量をアニメーション化するユニフォーム。正弦関数または余弦関数は -1 ~ 1 の範囲で値が変化するため、この目的に使用できます。
- JS のアニメーション ループ
ユニフォームは、MeshShaderMaterial と Vertex Shader の両方に追加します。まず、頂点シェーダーを作成します。
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);
12. まとめ
これで完了です。奇妙な(少しトリッピーな)脈動アニメーションが表示されます。
シェーダーには他にも多くのトピックがありますが、この概要がお役に立てば幸いです。これで、シェーダーを見たときに理解できるようになり、独自の素晴らしいシェーダーを自信を持って作成できるはずです。