Introduction to Shaders
ついにshadersの世界へ飛び込む時が来ました。shadersはあらゆる種類の視覚効果を作成するためには不可欠です。この章では、shadersとは何か、それで何ができるのか、そしてそれをReact Three Fiberでどのように使用するのかを学びます。
プレリュード
始める前に、shadersに慣れるまでに少し時間がかかるかもしれないことをお伝えしたいと思います。これまで書いてきたコードとは異なる動作をします。これは新しい発想と視覚効果の作成方法です。でも心配しないでください。プロセスを案内しますので、基礎から始めて、徐々により高度なトピックに進んでいきます。
最初はすべてを理解できなくても落胆しないでください。これは新しい概念であり、慣れるには練習が必要です。時間をかけて、実験し、練習することをお勧めしますが、それだけの価値はあります! shadersは非常に強力で、想像できるあらゆる視覚効果を作成するためのコントロールを提供します。
さらに、人それぞれ異なる学習スタイルがあります。読んで学ぶのが得意な人もいれば、ビデオを見て学ぶのが得意な人、実際にやってみて学ぶのが得意な人もいます。このトピックに関しては、異なるソースを相互参照することが非常に役立ちます。この章の終わりに知識を統合し、ここでカバーする以上のことを学ぶためのリソースを共有します。
怖がらせるつもりはありませんし、shadersを学ぶことにワクワクしていることを願っています。さあ、始めましょう!
Shadersとは何か?
ShadersはGPU(グラフィックス処理装置)上で実行される小さなプログラムです。GLSL(OpenGL Shading Language)というCに似た言語で書かれています。
Shadersは、メッシュの頂点を配置するためのもの(Vertex Shader)であり、面の各ピクセルを彩色するもの(Fragment Shader)です。
実際には私たちは常にshadersを使用してきました。materialを作成する際もshadersを使用しています。たとえば、MeshBasicMaterial
を作成する際には、メッシュを単一の色で彩色するshaderを使用しています。MeshStandardMaterial
を作成する際には、照明、影、および反射をシミュレートするshaderを使用しています。
Vertex Shader
Vertex Shaderはジオメトリの各頂点に対して実行されるプログラムです。その主な役割は、頂点を3D空間(私たちの3Dワールド)から2D空間(スクリーンやビューポート)へ変換することです。この変換は以下のいくつかの行列を使用して実現されます:
- View Matrix: この行列はシーン内のカメラの位置と方向を表します。頂点をワールド空間からカメラ空間へ変換します。
- Projection Matrix: パースペクティブまたはオルソグラフィックのどちらかのこの行列は、頂点をカメラ空間から正規化デバイス座標(NDC)に変換し、それらを2Dスクリーンへの最終的な投影の準備をします。
- Model Matrix: この行列はシーン内の各オブジェクトの位置、回転、スケールをカプセル化します。頂点をオブジェクト空間からワールド空間へ変換します。
さらに、Vertex Shaderは頂点の元の位置およびそれに関連するその他のattributesも取り入れます。
ジオメトリの各頂点に対して、Vertex Shaderが実行されます。
最終的に、2D空間での変換後の頂点の位置は、事前定義された変数 gl_Position
を介して返されます。すべての頂点が変換されると、GPUはそれらの間の値を補間してジオメトリの面を生成し、それがラスター化されてスクリーンに描画されます。
フラグメントシェーダー
フラグメントシェーダー(ピクセルシェーダーとも呼ばれる)は、ラスタライズプロセスによって生成された各フラグメント(またはピクセル)に対して実行されるプログラムです。主なタスクは、画面上の各ピクセルの最終的な色を決定することです。
ラスタライズ中に生成された各フラグメントに対して、フラグメントシェーダーが実行されます。
フラグメントシェーダーは、頂点シェーダーからの補間された値(色、テクスチャ座標、法線、およびジオメトリの頂点に関連するその他の属性)を受け取ります。これらの補間された値はvaryingsと呼ばれ、各フラグメント位置の表面特性に関する情報を提供します。
補間された値に加えて、フラグメントシェーダーはテクスチャをサンプリングしたり、全てのフラグメントにわたって一定のuniform変数にアクセスしたりすることができます。これらのuniform変数は、光の位置、material特性、またはシェーディング計算に必要なその他のデータを表すことができます。
このレッスンの後半で、attributesとuniformsについて詳しく説明します。
入力データを使用して、フラグメントシェーダーはフラグメントの最終色を決定するためにさまざまな計算を行います。これには、複雑なライティング計算、テクスチャマッピング、シェーディング効果、またはシーン内の任意の視覚効果が含まれる場合があります。
色計算が完了すると、フラグメントシェーダーは定義済みの変数gl_FragColor
を使用してフラグメントの最終色を出力します。
シェーダーについてできるだけ簡単に説明しようとしましたが、技術的な詳細を意図的に省略しました。それでも少し抽象的かもしれませんね。試しに簡単なシェーダーを作成し、実際にどう動作するかを見てみましょう。
初めてのシェーダー
スターターパックを実行しましょう。画面中央に黒い平面が表示されるはずです:
ファイルShaderPlane.jsx
を開きましょう。このファイルには、simple meshとplane geometryが含まれており、基本的なmaterialが使用されています。このmaterialをカスタムシェーダmaterialに置き換えます。
shaderMaterial
シェーダmaterialを作成するには、DreiライブラリのshaderMaterial
関数を使用します。
この関数は3つのパラメータを取ります:
uniforms
: シェーダで使用されるuniform変数を含むオブジェクト。今は空にしておいてください。vertexShader
: 頂点シェーダのGLSLコードを含む文字列。fragmentShader
: フラグメントシェーダのGLSLコードを含む文字列。
ファイルの最上部に、新しいシェーダmaterialをMyShaderMaterial
という名前で宣言しましょう:
import { shaderMaterial } from "@react-three/drei"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` );
すぐにシェーダコードの詳細に触れます。
React Three Fiberで宣言的に使用できるようにするために、extend
メソッドを使用します:
import { extend } from "@react-three/fiber"; // ... extend({ MyShaderMaterial });
これで<meshBasicMaterial>
を新しいシェーダmaterialに置き換えることができます:
import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` ); extend({ MyShaderMaterial }); export const ShaderPlane = ({ ...props }) => { return ( <mesh {...props}> <planeGeometry args={[1, 1]} /> <myShaderMaterial /> </mesh> ); };
これで前と同じ黒い平面が表示されるはずです。まだ何も変更していませんが、カスタムシェーダmaterialを使用するようになりました。
動作しているか確認するために、フラグメントシェーダで返している色を変更してみましょう。gl_FragColor
行を次のように置き換えます:
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor
はフラグメントの色を表す定義済みの変数です。これは4つのコンポーネントを持つベクトル(vec4)で、赤、緑、青、およびアルファチャンネルを表します。各コンポーネントは0から1の間のfloatです。
最初のコンポーネントを1.0
に設定することで、赤チャンネルを最大値に設定し、赤色になります。
画面中央に赤い平面が表示されるはずです:
おめでとうございます!初めてのシェーダmaterialを作成しました。これはシンプルですが、良いスタートです。
シェーダコード
次に進む前に、より快適にシェーダを書くための開発環境をセットアップしましょう。
シェーダコードを書くには二つのオプションがあります:
- インライン: シェーダコードをJavaScriptファイルに直接書く。
- 外部:
.glsl
拡張子の別ファイルにシェーダコードを書いて、それをJavaScriptファイルにインポートする。
私は、通常シェーダコードがmaterialの宣言に近い場所にあるため、適切なmaterialファイル内でのインラインの方法を好みます。
しかし、書いたり読んだりするのを容易にするために、GLSL用のシンタックスハイライターを使うことをお勧めします。Visual Studio Code用のComment tagget templates拡張機能を使うことができます。テンプレート文字列内のGLSLコードをハイライトしてくれます。
インストールが完了したら、シンタックスハイライターを有効にするために、以下のコメントをシェーダコードの先頭に追加する必要があります:
const MyShaderMaterial = shaderMaterial( {}, /* glsl */ ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, /* glsl */ ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } ` );
テンプレート文字列内のGLSLコードがハイライトされたのがわかるはずです:
上の頂点シェーダが正しいシンタックスハイライトを持ち、読みやすくなっています。
これがインラインシェーダコードに必要なすべてです。シェーダコードを別々に保ちたい場合は、外部ファイルを使用することもできます。どうやるか見てみましょう。
GLSLファイルのインポート
まず、src
フォルダー内にshaders
という名前の新しいフォルダーを作成します。このフォルダー内にmyshader.vertex.glsl
とmyshader.fragment.glsl
という2つのファイルを作成し、それぞれのファイルに対応するシェーダーコードをコピーします。
myshader.vertex.glsl
:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
myshader.fragment.glsl
:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
使用する命名規則は自由です。また、多くのシェーダーを持っている場合は、それらをサブフォルダーにグループ化することもできます。
次に、これらのファイルをJavaScriptファイルにインポートできるようにするため、開発依存としてvite-plugin-glslプラグインをインストールする必要があります:
yarn add vite-plugin-glsl --dev
次に、vite.config.js
ファイルにプラグインをインポートし、それをplugins
配列に追加します:
import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import glsl from "vite-plugin-glsl"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), glsl()], });
これで、JavaScriptファイルにGLSLファイルをインポートし、シェーダーコードとして使用することができます:
import myShaderFragment from "./shaders/myshader.fragment.glsl"; import myShaderVertex from "./shaders/myshader.vertex.glsl"; const MyShaderMaterial = shaderMaterial({}, myShaderVertex, myShaderFragment);
これで、シェーダーコードを快適に作成およびインポートできるようになり、シェーダーコードのさまざまな部分を探索し始めることができます。
GLSL
シェーダーコードはGLSL(OpenGL Shading Language)で書かれています。Cに似た言語ですので、基本的な内容を見ていきましょう。
タイプ
GLSLにはいくつかのタイプがありますが、最も一般的なのは以下の通りです:
- bool: ブール値(
true
またはfalse
)。 - int: 整数。
- float: 浮動小数点数。
- vectors: 数のコレクション。
vec2
は2つの浮動小数点数のコレクション(x
とy
)、vec3
は3つの浮動小数点数のコレクション(x
、y
、およびz
)、およびvec4
は4つの浮動小数点数のコレクション(x
、y
、z
、およびw
)。x
、y
、z
、およびw
の代わりに、色の場合はr
、g
、b
、およびa
を使用できます。 - matrices: ベクターのコレクション。たとえば、
mat2
は2つのベクターのコレクション、mat3
は3つのベクターのコレクション、mat4
は4つのベクターのコレクションです。
スウィズリングと操作
スウィズリングを使用して、ベクターのコンポーネントにアクセスできます。たとえば、他のベクターのコンポーネントを使用して新しいベクターを作成することができます。
vec3 a = vec3(1.0, 2.0, 3.0); vec2 b = a.xy;
この例では、b
は a
の x
および y
コンポーネントを持つベクターになります。
コンポーネントの順序を変更するためにスウィズリングを使用することもできます:
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = a.zyx;
この例では、b
は vec3(3.0, 2.0, 1.0)
と等しくなります。
すべて同じコンポーネントを持つ新しいベクターを作成するために、コンストラクタを使用できます:
vec3 a = vec3(1.0);
この例では、a
は vec3(1.0, 1.0, 1.0)
と等しくなります。
演算子
GLSLには一般的な算術演算子: +
, -
, *
, /
, +=
, /=
, *=
および一般的な比較演算子: ==
, !=
, >
, <
, >=
, <=
があります。
これらは正しい型で使用する必要があります。例えば、整数と浮動小数点を加算することはできません。まず、整数を浮動小数点に変換する必要があります:
int a = 1; float b = 2.0; float c = float(a) + b;
また、ベクトルや行列にも演算を行うことができます:
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = vec3(4.0, 5.0, 6.0); vec3 c = a + b;
これは次のように書き換えることもできます:
vec3 c = vec3(a.x + b.x, a.y + b.y, a.z + b.z);
関数
頂点シェーダとフラグメントシェーダのエントリーポイントはmain
関数です。これはシェーダが呼び出されたときに実行される関数です。
void main() { // あなたのコードはこちら }
void は関数の戻り値の型です。これは関数が何も返さないことを意味します。
独自の関数を定義することもできます:
float add(float a, float b) { return a + b; }
この関数をmain
関数内で呼び出すことができます:
void main() { float result = add(1.0, 2.0); // ... }
GLSLには、sin
, cos
, max
, min
, abs
, round
, floor
, ceil
などの一般的な操作向けの組み込み関数が多数提供されています。また、mix
, step
, length
, distance
のような便利な関数もあります。
次のレッスンで、これらの基本的な関数を発見し、練習していきます。
ループと条件
GLSLは for
ループと if
ステートメントをサポートしています。それらはJavaScriptと同様に動作します:
for (int i = 0; i < 10; i++) { // Your code here } if (condition) { // Your code here } else { // Your code here }
ロギング/デバッグ
シェーダープログラムは並列に各頂点やフラグメントのために実行されるため、console.log
を使用してコードをデバッグしたり、ブレークポイントを追加することはできません。これがシェーダーのデバッグを難しくしている理由です。
シェーダーをデバッグする一般的な方法は、gl_FragColor
を使用して変数の値を視覚化することです。
コンパイルエラー
シェーダーコードに間違いがある場合、コンソールにコンパイルエラーが表示されます。エラーの行と種類を教えてくれます。理解するのは簡単ではないかもしれませんが、問題を見つけるための良い指標になります。
アルファチャネルを gl_FragColor
から削除してみて、何が起こるか見てみましょう:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0); }
コンソールにコンパイルエラーが表示されるはずです:
gl_FragColor
は4つのコンポーネントを期待していますが、3つしか提供していないと通知されています。
エラーを取り除くためにアルファチャネルを 1.0
に戻すことを忘れないでください。
Uniforms
JavaScriptコードからshaderにデータを渡すためには、uniformsを使用します。uniformsは全ての頂点とフラグメントにおいて一定です。
projectionMatrix
、modelViewMatrix
、およびposition
は、shaderに自動的に渡されるビルトインのuniformsの例です。
カスタムのuniformを作成して、色をshaderに渡しましょう。これを使用してplaneを着色します。これをuColor
と呼びます。uniformの名前の前にu
を付けるのは、それがコードにおいてuniformであることを明確にするための良い習慣です。
まず、shaderMaterial
のuniformのオブジェクトでそれを宣言します:
import { Color } from "three"; // ... const MyShaderMaterial = shaderMaterial( { uColor: new Color("pink"), } // ... ); // ...
次に、フラグメントシェーダーでそれを使用します:
uniform vec3 uColor; void main() { gl_FragColor = vec4(uColor, 1.0); }
ピンク色に着色されたplaneが見えるはずです:
ここで、ピンク色はuniformのデフォルト値です。material上で直接変更できます:
<MyShaderMaterial uColor={"lightblue"} />
planeは今、ライトブルーに着色されています。
頂点シェーダーとフラグメントシェーダーの両方がuniformsにアクセスできます。時間を第二のuniformとして頂点シェーダーに追加し、planeを上下に動かしましょう:
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.