チュートリアル シェーダー入門

シェーダー入門

By Dave Pagurek, Austin Lee Slominski, Adam Ferriss

現代のコンピューターには、グラフィックス処理ユニット(GPU)と呼ばれる特別なハードウェアが搭載されています。シェーダーはGPU上で動作する特殊なプログラムで、驚くべきことを実現できます。GPUを活用して多くのピクセルを同時に並列処理することで、高速で特にコンピューターグラフィックスの特定のタスク(ノイズの生成、ぼかしなどのフィルターの適用、ポリゴンのシェーディングなど)に適しています。

シェーダープログラミングは最初は難しく感じるかもしれません。p5.jsの2D描画とは異なるアプローチが必要です。このチュートリアルでは、シェーダープログラミングの基本を概説し、他のリソースを紹介します。

セットアップ

ブラウザでGPUをプログラムする方法は、WebGLと呼ばれるAPIを使用することです。p5.jsはシェーダーを扱うのに優れたツールです。多くのWebGLのボイラープレートセットアップを処理してくれるので、シェーダーコード自体に集中できるからです。シェーダーを始める前に、p5.jsのキャンバスをWebGLモードで設定する必要があります。これはcreateCanvas()の3番目のパラメーターにWEBGL定数を追加することで行います。

...

function setup() {
  createCanvas(200, 200, WEBGL);
}

...

シェーダープログラムは、頂点シェーダーフラグメントシェーダーの2つの部分で構成されています。頂点シェーダーは、ジオメトリの各頂点に対して1回実行されるプログラムで、画面上のどこに描画されるかを決定します。フラグメントシェーダーは、そのジオメトリの各ピクセルに対して1回実行されるプログラムで、その色を決定します。

赤い球体

赤い球体

時間とともに揺れ歪む赤い球体

時間とともに揺れ歪む赤い球体

赤と青の縞模様で塗りつぶされた揺れる球体のシルエット

赤と青の縞模様で塗りつぶされた揺れる球体のシルエット

元の形状

カスタム頂点シェーダーは形状内の頂点の位置を調整できます

カスタムフラグメントシェーダーは形状内の色を調整できます

これらはそれぞれ別のファイルに保存され、loadShader()関数を使用してp5.jsにロードされます。シェーダーがロードされると、setup()またはdraw()内で使用できます。以下の例は、p5.js内で基本的なシェーダーをセットアップする方法を示しています:

let myShader;
function preload() {
  // 各シェーダーファイルをロードします(心配しないでください、これらについては後で説明します!)
  myShader = loadShader('shader.vert', 'shader.frag');
}
function setup() {
  // キャンバスはWEBGLモードで作成する必要があります
  createCanvas(windowWidth, windowHeight, WEBGL);
}
function draw() {
  // shader()はアクティブなシェーダーを設定し、次に描画されるものに適用されます
  shader(myShader);
  // シェーダーをキャンバス全体を覆う長方形に適用します
  plane(width, height);
}

また、createShader()という追加の関数もあり、これを使用してスケッチ内で定義された文字列から直接シェーダーをロードできます。

シェーダーの記述

次に、loadShader()で参照した頂点シェーダーとフラグメントシェーダーのファイルの中身を見てみましょう。

シェーディング言語(GLSL)

シェーダーファイルはグラフィックスライブラリシェーディング言語、つまりGLSL(OpenGL 2.0とGLSL ES 1.00に基づく)で書かれており、私たちが慣れ親しんでいるものとは非常に異なる構文と構造を持っています。GLSLはCに似た構文を持っており、JavaScriptには存在しない概念がいくつか含まれています。

まず、シェーディング言語は型に関してはるかに厳密です。作成する各変数には、それが格納しているデータの種類をラベル付けする必要があります。以下は一般的な型のリストです:

vec2(x,y)     // 2つのfloatからなるベクトル
vec3(x,y,z)   // 3つのfloatからなるベクトル(r,g,bでもよい)
vec4(x,y,z,w) // 4つのfloatからなるベクトル(r,g,b,aでもよい)
float         // 小数点を持つ数値
int           // 小数点のない整数
sampler2D     // テクスチャへの参照
mat2          // 2x2行列
mat3          // 3x3行列
mat4          // 4x4行列
bool          // trueまたはfalse

一般的に、シェーディング言語はJavaScriptよりもはるかに厳密です。セミコロンの欠落は許されず、エラーメッセージが表示されます。floatや整数など、異なる種類の数値を互換的に使用することはできません。また、小数点なしで定義されたfloatについても警告が出るため、0.01.0のような数値をよく目にすることになります。

GLSLで異なる点をいくつか紹介します:

Javascript

GLSL

すべての変数に型が必要です。

let a = 1;
let b = 0.5;
int a = 1;
float b = 0.5;

関数はパラメーターの型と戻り値の型を宣言する必要があります。

function isBetween(val, start, end) {
  return val >= start && val <= end;
}
bool isBetween(float val, float start, float end) {
  return val >= start && val <= end;
}

整数とfloatの間の変換は自分で行う必要があります。

let a = 1;
let b = 0.5;
let c = b + 2;
let d = a + b;
int a = 1;
float b = 0.5;
float c = b + 2.0;
float d = float(a) + b;

GLSLのループは定数値で停止する必要があります。条件付きで終了したい場合は、breakを使用してループから抜け出すことができます。

let maxVal = 10;
if (something) {
  maxVal = 20;
}
for (let i = 0; i < maxVal; i++) {
  // 何かを実行
}
int maxVal = 10;
if (something) {
  maxVal = 20;
}
for (let i = 0; i < 20; i++) {
  if (i == maxVal) {
    break;
  }
  // 何かを実行
}

多くの制約がある一方で、GLSLの方が扱いやすい面もあります!ベクトルを使用する際、GLSLには多くの便利なショートカットが含まれています:

vec4を持っている場合、そのデータを色や座標のように参照できます。両方が同等なので、コードを読みやすくするためにどちらでも使用できます。

//各ペアは同等です:
myVec.x
myVec.r
myVec.y
myVec.g
myVec.z
myVec.b
myVec.w
myVec.a

すべての値が同じベクトルを作成したい場合、同じ値を繰り返し指定する必要はなく、1回だけ指定すれば十分です。

// これらは同等です
myVec = vec3(0.5, 0.5, 0.5);
myVec = vec3(0.5);

「スウィズリング」と呼ばれる方法を使用して、大きなベクトルから小さなベクトルを取得できます。これは、.の後に複数のプロパティ値を希望の順序で連結することで行います。

vec4 bigVec = vec4(1.0, 2.0, 3.0, 4.0);
// vec2(bigVec.z, bigVec.y)と同等
vec2 smallVec = bigVec.zy;

頂点シェーダー

以下は、p5.jsによって提供される変換とカメラの視点を適用する簡単な頂点シェーダーです:

precision highp float;

シェーダーはprecision行から始まります。これはlowpmediump、またはhighpのいずれかになります。最高品質を使用することは、シェーダーがどこでも同じように見えることを確実にするための良い出発点です。デスクトップやラップトップでは、あなたが何を書いても、GPUは恐らく最高品質を使用します。携帯電話では、低い品質を使用するとより高速になる可能性がありますが、シェーダーの描画が異なる可能性があります。

attribute vec3 aPosition;

シェーダーの属性には、頂点ごとに変化する値が含まれており、p5.jsはこれを使用して各頂点の位置などの情報を共有します。このシェーダーの属性はvec3で、x、y、zの値を含んでいます。属性は頂点シェーダーでのみ使用される特別な変数型で、通常はp5.jsによって提供されます。p5.jsのメソッド(rect()vertex()など)を使用すると、p5.jsは自動的に頂点情報をシェーダーに渡します。

// 描画されるオブジェクトの変換
uniform mat4 uModelViewMatrix;
// 3D座標を2Dスクリーン座標に
// 変換します
uniform mat4 uProjectionMatrix;

シェーダーのユニフォームは、描画される形状全体で一定の値です。このシェーダーの各ユニフォームはmat4で、これは平行移動、スケール、回転などの変換を表すためによく使用される型です。点をmat4で乗算すると、その点に変換が適用されます。このシェーダーのユニフォームはp5.jsによって自動的に提供されますが、後で独自のカスタムユニフォームを提供する方法を見ていきます。行列との乗算の順序は重要です。ほとんどの場合、行列を最初に書き、それによって乗算される値を2番目に書きます。

void main() {
  // カメラの変換を適用
  vec4 viewModelPosition =
    uModelViewMatrix * vec4(aPosition, 1.0);
  // WebGLに頂点の位置を伝える
  gl_Position =
    uProjectionMatrix * viewModelPosition;  
}

すべての頂点シェーダーにはmain()関数が必要で、その中でgl_Positionに値を割り当てることで頂点の位置を決定します。この値はクリップ空間にあり、x、y、z値は一方の端から他方の端に-1から1の範囲で変化します。3D点をuProjectionMatrixで乗算することで、p5.jsのカメラ設定を使用してこの変換を行います。その前に、このシェーダーはuModelViewMatrixを乗算して、形状を描画する前に設定された累積変換を適用します。

これがまだ十分に理解できなくても心配しないでください。頂点シェーダーは重要な役割を果たしますが、多くの場合、フラグメントシェーダーで作成したものをジオメトリ上に正しく表示するだけの責任があります。おそらく、多くのプロジェクトで同じ頂点シェーダーを再利用することになるでしょう。以下は、頂点ごとの色やテクスチャ座標などの情報も扱う標準的な頂点シェーダーです。

precision highp float;
attribute vec3 aPosition;
attribute vec2 aTexCoord;
attribute vec4 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec2 vTexCoord;
varying vec4 vVertexColor;
void main() {
  // カメラの変換を適用
  vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0);
  // WebGLに頂点の位置を伝える
  gl_Position = uProjectionMatrix * viewModelPosition;  
  // データをフラグメントシェーダーに渡す
  vTexCoord = aTexCoord;
  vVertexColor = aVertexColor;
}

フラグメントシェーダー

フラグメントシェーダーは、シェーダーの色出力を担当し、ここで多くのシェーダープログラミングを行います。以下は、単に赤色を表示する非常に簡単なフラグメントシェーダーです:

precision highp float;

フラグメントシェーダーも、float precisionを指定する行から始まります。これは頂点シェーダーのprecisionと一致する必要があります。

void main() {
  vec4 myColor = vec4(1.0, 0.0, 0.0, 1.0);
  gl_FragColor = myColor;
}

頂点シェーダーと同様に、フラグメントシェーダーもmain()関数が必要ですが、gl_Positionを設定する代わりに、GLSLによって定義された特別な変数であるgl_FragColorに色を割り当てます。

変数myColorvec4として定義されており、4つの値を格納します。色を扱っているので、これら4つの値は赤、緑、青、アルファです。シェーダーは、デフォルトのp5.jsスケッチのように0-255を使用せず、代わりに0.0から1.0の間の値を使用します。

これで頂点シェーダーとフラグメントシェーダーができたので、これらを別々のファイル(それぞれshader.vertshader.frag)に保存し、loadShader()を使用してスケッチにロードすることができます。

ユニフォーム:スケッチからシェーダーへのデータ渡し

このような単純なシェーダーは、それ自体で有用な場合もありますが、p5.jsスケッチからシェーダーに変数を伝える必要がある場合があります。これがユニフォームが必要になる時です。ユニフォームは、スケッチからシェーダーに送ることができる変数の一種です。これにより、JavaScriptからシェーダーをより細かく制御することが可能になります。

ユニフォームは、main()の外側のファイルの先頭で定義されます。頂点シェーダーとフラグメントシェーダーの両方でアクセスできます。以下の例では、p5.jsメソッドmillis()から返される値が’time’ユニフォームに渡され、頂点シェーダーに動きを導入しています。

これはフラグメントシェーダーでも機能します。次の例では、JavaScriptのスケッチ部分から色を変更できるようにする色のユニフォームmyColorを作成します。シェーダーでは色チャンネルの値が0-255ではなく0-1の範囲であることを忘れないでください。

p5.jsが提供するユニフォームの完全なリストは、p5.js WebGLモードアーキテクチャドキュメントで確認できます。

Varyings: 頂点シェーダーからフラグメントシェーダーへのデータ受け渡し

Varying変数は、頂点シェーダーとフラグメントシェーダーの間でデータを共有します。これにより、フラグメントシェーダー内で位置やその他のジオメトリデータを使用することが可能になります。

例えば、フラグメントシェーダーで形状のテクスチャ座標を使用したい場合があります。これらはvec2の形式で提供され、座標は0から1の間の値を取ります。これは最初、p5.jsからattributeとして提供され、頂点シェーダーでのみアクセス可能です。標準の頂点シェーダーがこれをフラグメントシェーダーに渡す方法を見てみましょう:

precision highp float;

attribute vec3 aPosition;
attribute vec2 aTexCoord;

テクスチャ座標は最初、aTexCoordという名前のattributeとして提供されます。これはp5.jsによって自動的に設定されます。

attribute vec4 aVertexColor;

uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec2 vTexCoord;

ここで、varying変数を宣言します。頂点シェーダーで宣言したvaryingは、フラグメントシェーダーでも再度宣言でき、そこで頂点シェーダーによって割り当てられた値にアクセスできます。

varying vec4 vVertexColor;
void main() {
  // カメラ変換を適用
  vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0);
  // WebGLに頂点の位置を伝える
  gl_Position = uProjectionMatrix * viewModelPosition;
  vVertexColor = aVertexColor;

attributeの値をvarying変数に割り当てることで、フラグメントシェーダーが読み取れる場所にデータをコピーしています。

}

頂点シェーダーでvTexCoordというvaryingを定義したので、フラグメントシェーダーでもそれを使用できるようになりました。以下は、x値を赤チャンネルに、y値を青チャンネルにマッピングする簡単なフラグメントシェーダーです。vTexCoordは頂点シェーダーでは頂点ごとに定義されますが、フラグメントシェーダーではピクセルごとに値が定義されることに注意してください。ピクセルごとの値を得るために、WebGLは各面の頂点値間をスムーズに補間します。

precision highp float;
varying vec2 vTexCoord;
void main() {
  // 座標をシェーダーの色出力に割り当てる
  gl_FragColor = vec4(vTexCoord.x, vTexCoord.y, 1.0, 1.0);
}

plane(width, height)にこのシェーダーを使用した結果:

左上が黒、右上がマゼンタ、右下が白、左下がシアンの長方形のグラデーション。

フィルターシェーダー

p5.jsでは、フィルターはキャンバス上のすべてのピクセルを見て、それらを別のものに置き換えるものです。キャンバスの色を反転させたり、キャンバスの内容にぼかしを適用したりする多くの組み込みフィルターがあります。フラグメントシェーダーを書くことで、独自のフィルターを作成できます。

フィルターシェーダーにはフラグメントシェーダーだけが必要です。頂点シェーダーは主に形状の位置決めを担当し、フィルターは常にキャンバス全体に適用されるため、p5.jsがデフォルトの頂点シェーダーを提供します。loadShaderの代わりに、createFilterShader(src)を使用し、シェーダーのソースコードを含む文字列を渡します。

フィルターシェーダーで利用可能なuniformがいくつかあり、それらすべてについてはcreateFilterShaderのドキュメントで読むことができます。始めるにあたって知っておくべき主な2つは以下の通りです:

  • uniform sampler2D tex0はキャンバスの内容を含むテクスチャです。
  • varying vec2 vTexCoordには現在のピクセルのキャンバス上の座標が含まれており、0から1の範囲です。

これらを組み合わせると、texture2D(tex0, vTexCoord)はキャンバス上の現在のピクセルの色を返し、それを修正できます。この例では、赤と緑のチャンネルを青チャンネルで置き換えることで、カスタムの白黒フィルターを作成します:

試してみたいもう一つのことは、texture2D出力ではなく入力を修正することです。使用するテクスチャ座標を調整することで、元の画像からのオフセットを作成したり、ピクセルごとにオフセットが異なる場合はワープ効果を作成したりできます:

まとめ

これらのスキルを使って基本的なシェーダーを作成できますが、シェーダープログラミングはさらに深く進むことができ、このチュートリアルの範囲を超える多くのシェーダーのトピックがあります。p5.jsのシェーダーは、ビジュアル、エフェクト、テクスチャを作成し、3Dジオメトリにマッピングするための強力なツールとなります。

シェーダーについてもっと学びたいですか?これらのウェブサイトをチェックしてみてください!

用語集

シェーダー

多くのビジュアルエフェクトやフィルターを効率的に生成できる特別なグラフィックスカードプログラム。

GLSL

Graphics Library Shader Language(GLSL)は、シェーダーを書くために使用されるプログラミング言語です。

Uniform

スケッチからシェーダーに渡される変数。

Varying

頂点シェーダーからフラグメントシェーダーに渡される変数

ベクトル(vec2 / vec3 / vec4

数値のグループを格納するデータ型で、最も一般的には2つ、3つ、または4つの数値を格納し、色、位置などを表現します。

Float

小数点を持つ浮動小数点数を格納するデータ型。

Int

小数点のない整数を格納するデータ型。

Sampler

シェーダーに渡されるテクスチャを表すデータ型。GLSLでは通常sampler2Dとして表現されます。

Attribute

p5.jsスケッチで生成され、頂点シェーダーで利用可能になるGLSL変数。ほとんどの状況では、これらはp5.jsによって提供されます。

Texture

シェーダープログラムに渡される画像。texture2D()関数を使用してサンプリングできます。

Type

int、float、vectorなど、データの形式を説明するラベル。

Vertex Shader

3D空間でジオメトリの位置決めを担当するシェーダープログラムの部分。

Fragment Shader

シェーダーによって出力される各ピクセルの色と外観を担当するシェーダープログラムの部分。