シェーダーの概要

はじめに

以前、Three.js の概要について説明しました。この記事をご覧になっていない場合は、ぜひお読みください。この記事では、これを基盤として構築を行います。

これから、シェーダーについて考えてみましょう。WebGL は優秀で、Three.js(およびその他のライブラリ)の前でも述べたように、難題を抽象化して優れた機能を発揮します。しかし、特定の効果を実現したい場合もありますし、そのような素晴らしい要素が画面にどのように表示されていたかをもう少し深く掘り下げたい場合もあります。そのため、シェーダーはほぼ間違いなくその一部です。また、私のような場合は、前回のチュートリアルの基本的な内容から、もう少し複雑なものに進むのがよいでしょう。シェーダーの開始に関して、面倒な作業の多くを Three.js が行うため、ここでは Three.js を使用していることを前提とします。最初にシェーダーのコンテキストについて説明します。このチュートリアルの後半では、少し高度な内容について説明します。その理由は、シェーダーが一見したところ一般的ではなく、少し説明できることにあります。

1. 2 つのシェーダー

WebGL では、固定パイプラインの使用は提供されません。これは、すぐに使用できるものをレンダリングする手段がないことを意味します。その機能である Programmable Pipeline は、より強力ですが、理解や使用が困難です。簡単に言うと、プログラマブル パイプラインは、プログラマが頂点などを画面にレンダリングする責任を負うことを意味します。シェーダーはこのパイプラインの一部であり、次の 2 種類があります。

  1. 頂点シェーダー
  2. フラグメント シェーダー

どちらも、それだけでは何の意味も持ちません。どちらも、グラフィック カードの GPU で完全に実行されます。つまり、可能な限りすべてを GPU にオフロードし、CPU に他の処理を任せたいということです。最新の GPU はシェーダーに必要な機能に対して高度に最適化されているため、使用できると便利です。

2. 頂点シェーダー

球などの標準的なプリミティブ シェイプを用意します。頂点で構成されていますよね。頂点シェーダーには、これらの頂点が 1 つずつ順番に渡され、頂点の操作が可能です。各頂点に対して実際に何を行うかは頂点シェーダー次第ですが、1 つの責任があります。それは、ある時点で gl_Position という 4 次元浮動小数点ベクトルを設定することです。これは、画面上の頂点の最終的な位置です。これはそれ自体が非常に興味深いプロセスです。実際には、3D 位置(x、y、z を持つ頂点)を 2D 画面に取得または投影しています。幸いなことに、Three.js のようなものを使用している場合は、負荷をかけずに gl_Position を簡単に設定できます。

3. フラグメント シェーダー

頂点を持つオブジェクトが作成され、2D 画面に投影されましたが、使用する色はどうすればよいでしょうか。テクスチャやライティングについてはどうですか?フラグメント シェーダーはまさにそのためのものです。頂点シェーダーと非常によく似て、フラグメント シェーダーも必須の処理が 1 つだけあります。それは、フラグメントの最終的な色である別の 4D 浮動小数点ベクトルである gl_FragColor 変数を設定または破棄することです。では、フラグメントとは何でしょうか。三角形を構成する 3 つの頂点について考えてみましょう。その三角形内の各ピクセルを描画する必要があります。フラグメントは、その三角形の各ピクセルを描画するために、3 つの頂点から提供されるデータです。このため、フラグメントは、その構成要素である頂点から補間された値を受け取ります。1 つの頂点が赤色で、隣接する頂点が青色の場合、色値は赤色から紫色を経て青色に補間されます。

4. シェーダー変数

変数には、ユニフォーム属性変化の 3 つの宣言があります。これらの 3 つを初めて聞いたとき、これまで扱ってきたものとは異なるため、非常に混乱しました。次のように考えてみましょう。

  1. ユニフォームは、頂点シェーダーとフラグメント シェーダーの両方に送信され、レンダリングされるフレーム全体で同じ値が保持されます。たとえば、照明の位置がこれに該当します。

  2. 属性は、個々の頂点に適用される値です。属性は頂点シェーダーでのみ使用できます。たとえば 頂点ごとに色が異なるといった ケースです属性は頂点と 1 対 1 の関係にあります。

  3. 変化量は、頂点シェーダーで宣言され、フラグメント シェーダーと共有する変数です。これを行うには、頂点シェーダーとフラグメント シェーダーの両方で、同じ型と名前の可変変数を宣言します。頂点の法線はライティングの計算に使用できるため、古典的には頂点の法線が用いられます。

後ほど、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 対 1 の関係が必要です。

7. 次のステップ

次に、アニメーション ループ、頂点属性、ユニフォームを追加します。また、頂点シェーダーがフラグメント シェーダーにデータを送信できるように、可変変数も追加します。最終的な結果は、ピンク色だった球体が上部と側面から照らされ、脈動するように見えることです。少し複雑ですが、3 つの変数型と、それらが相互にどのように関連し、基盤となるジオメトリとどのように関連しているかを理解するのに役立つことを願っています。

8. 偽の光

フラットな色のオブジェクトにならないように、色付けを更新しましょう。Three.js によるライティングの処理方法を確認することはできますが、おわかりのように、現時点では必要以上に複雑であるため、フェイクします。Three.js の一部である素晴らしいシェーダーや、Chris Milk と Google、Rome による最近の素晴らしい WebGL プロジェクトのシェーダーをぜひお読みください。シェーダーに戻りましょう。Vertex シェーダーを更新して、各頂点を Fragment シェーダーに正規化します。そのため、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~1 の範囲で値が変化するため、この目的に使用できます。
  2. 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. まとめ

これで完了です。奇妙な(そしてややトリッピーな)点滅するアニメーションになっているのがわかります。

シェーダーには他にも多くのトピックがありますが、この概要がお役に立てば幸いです。これで、シェーダーを見たときに理解できるようになり、独自の素晴らしいシェーダーを自信を持って作成できるはずです。