HN 日本語サマリー

← 一覧へ戻る
プログラミング

Godot 4でゼロから始める最初のシェーダー

First Shader from Zero in Godot 4 (gdquest.com)

75 pointsby ibobev8 コメント

要約

この記事は、Godot 4エンジンを使用して、プログラミング経験が全くない初心者でもシェーダーを作成する方法を解説する学習リソースです。2026年9月5日に作成され、9日に更新されたこのガイドは、シェーダーの基本概念から実践的な応用までを網羅しています。

全文翻訳

Godot 4でゼロから始める最初のシェーダー

  1. ライブラリ
  2. 学習リソース
  3. シェーダー

Godotのようなモダンなゲームエンジンは、内部で多くのシェーダーを使用しています。

シェーダーとは? それは、コンピューターのグラフィックカード上で直接実行されるプログラムです。シェーダーは、GPUが3Dモデル、パーティクル、2Dスプライトなどを画面にどのように描画するかを制御します。

ゲーム制作を始めたばかりの頃は、必ずしもシェーダーをプログラミングする必要はありません。Godotは、コードを書かずに調整できる多くの組み込みビジュアルエフェクトを提供しています。また、Godotコミュニティは、Godot ShadersやGDQuestの無料・オープンソースシェーダーコレクションで見つけられる多くの無料シェーダーを作成しています。それらのコードをゲームにコピーして、出発点として使用できます。

しかし、望む通りの見た目を実現したり、ユニークなビジュアルエフェクトを作成したり、ゲームの実行速度を向上させたりするためには、最終的にはシェーダーをコーディングする方法を学ぶ必要があります。

例えば、ゲームで光るポータル、風になびく草、プラットフォーマーゲームの2Dアニメーション水面などを見たことがあるでしょう。これらのエフェクトはシェーダーによって動いています。

このサイドスクロールゲームの水面は、シェーダーコードで完全に描画・アニメーションされています。

この詳細な初心者向けガイドは、あなたが始めるのに役立ちます。Godot 4で最初の2Dシェーダーを書く方法を教えます。

このアニメーションポータルシェーダーを、一歩ずつ作成していきます。

このエフェクトは、あらゆる種類の2Dおよび3Dビジュアルエフェクトに必要な、多くの転用可能なテクニックを教えてくれます。

  • 数学を使って単純な図形を描画する
  • smoothstep()関数を使ってソフトなエッジと遷移を作成する
  • マスキングを使用して複数の図形を組み合わせて複雑な図形を作成する
  • 周期的関数を使用して時間をかけてビジュアルをアニメーションさせる
  • ノイズを使用してより自然に見えるエフェクトを作成する
  • テクスチャをサンプリングして任意の画像から色を読み取り、ビジュアルをシェーダーに取り込む

また、シェーダーが従来のプログラムとどのように異なるか、そしてそれらに必要な考え方についても学びます。複雑なビジュアルエフェクトを小さく管理しやすい問題に分解することで、テクニカルアーティストのように考える方法を学びます。

これは、経験豊富なアーティストがこのコースで学ぶシェーダーコードを基盤として構築することで達成できることです。

注記:

コースの最後には、アーティスト版のダウンロードが含まれています。もしそれを探求したい場合は、そちらをご覧ください。

前提条件

このガイドは、シェーダーやグラフィックスプログラミングの経験がない人を対象としています。しかし、ビジュアルエフェクトに焦点を当てているため、変数やループのような基本的なプログラミング概念は教えません。GDScriptとGodotの基本はすでに知っていると仮定します。

以下の内容に慣れている必要があります。

  • Godot 4エディターの操作
  • ノードの作成とプロパティの変更
  • 一般的なGDScriptコードの読み書き

Godotが初めての場合は、基本を学ぶのに役立つ無料のリソースがいくつかあります。

このチュートリアルは、現在GDQuestのカリキュラムに従ってGodotでゲームを作る方法を学んでいる、新旧の開発者全員のサポートによって提供されています。

Learn 2D Gamedev From Zero with Godot 4 やその他のGDSchoolのコースを購入された方、ありがとうございます!皆様のサポートが、このような無料ガイドをすべての人に提供できる原動力となっています。

Nathan
GDQuestの創設者兼教師

シェーダーは、従来のプログラミングとは異なる考え方を必要とするため、恐ろしく見えることがあります。ゲームプレイコードはシーケンシャルですが、シェーダーコードはグラフィックカード上で完全に並列に実行されます。

これは大規模なガイドなので、ブックマークして、数回のセッションに分けて作業し、後で参照のために戻ってきてください!あらゆるプログラミングと同様に、シェーダーに慣れるには時間と練習が必要です。

コーディングを始めるにあたって迷わないように、理論の最小限の内容から始めましょう。

注記:

ヘルプが必要な場合、問題に遭遇した場合、またはフィードバックを共有したい場合は、Discordサーバーのfirst-shader-godot-4チャンネルに投稿できます。

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

導入で述べたように、シェーダーはグラフィックカード(Graphics Processing Unit、略してGPU)に画面にどのように描画するかを指示するプログラムです。

画面に描画するために、GPUはレンダリングパイプラインと呼ばれる一連の定義済みステップを経由します。これらのステップは、図形と画像のような入力データを、画面に見えるピクセルに変換します。デフォルトでは、エンジンは提供されたテクスチャ画像を取得し、Sprite2Dノードの境界内に描画します。シェーダーを使用して、これらのステップのデフォルトの動作を変更できます。

Godotでシェーダーコーディングを開始するには、パイプラインの2つのステージについて知っておくだけで十分です。それは頂点シェーダーとフラグメントシェーダーです。

このガイドではフラグメントシェーダーをコーディングします。

このガイドでは、フラグメントシェーダーのみをコーディングします。これは2Dゲームで最もよく使用するシェーダータイプです。しかし、フラグメントシェーダーがピクセルを塗りつぶす図形を準備するため、頂点シェーダーも簡単に確認する必要があります。

要するに、頂点シェーダーは外側の幾何学的形状を制御し、フラグメントシェーダーはその形状内のピクセルを描画します。

頂点シェーダー

グラフィックカードについて知っておくべきことが一つあります。それは、ピクセルで塗りつぶされる三角形しか描画できないということです。例えば、Sprite2Dノードは2つの三角形で構成される長方形です。この長方形内で、GPUは透明および不透明なピクセルをペイントしてスプライトの外観を作成します。

したがって、オブジェクトを画面に描画するために、GPUはまずオブジェクトの形状を決定する必要があります。これは頂点を使用して行われます。頂点とは、その形状を定義する点です。

3Dモデルを作成したことがあるなら、頂点にはすでに慣れているでしょう。それらは、3Dアートプログラムで作業する際に編集できるすべての角の点です。この画像では、3DアートプログラムBlenderで立方体の1つの頂点を選択しました。

[画像: The vertex of a 3D cube]

3D立方体の頂点

しかし、頂点は3Dモデル専用ではありません。スプライトのようなフラットな2D要素も幾何学的形状を持つ必要があります。例えば、Godotでは、Sprite2Dノードの長方形の形状は4つの頂点で定義されます。4つの頂点は、ここではひし形として表されています。

[画像: The vertices of a Sprite2D]

他の2Dノード、例えばPolygon2Dは、より複雑な形状を作成するために4つ以上の頂点を持つことができます。

[画像: The vertices of a Polygon2D]

デフォルトでは、GPUはノードの形状を、頂点をそのまま使用し、画面上に配置することで与えます。頂点シェーダーを使用すると、各頂点の位置を変更し、それによってオブジェクト自体の形状を歪ませたりオフセットしたりできます。

Godotでは、特定の名前を持つ関数を使用して、各シェーダーのステージにどのコードを実行するかをGPUに伝えます。頂点シェーダーを使用するには、シェーダーコードでvertex()という名前の関数を定義します。

vertex()関数を定義すると、GPUはすべての頂点に対して並列に実行します。このガイドでは、ポータルの外側の長方形を変更する必要がないため、この関数は必要ありません。

レンダリングパイプラインで常にフラグメントシェーダーの前に実行されるため、ここで説明しています。シェーダーを扱う際には、常にこれに遭遇することになります。

このガイドでは頂点シェーダーコードを記述しませんが、それが存在することとパイプライン内のどこに位置するかを知ることは、シェーダーに関する学習リソースや他人のシェーダーコードを理解するのに役立ちます。

OldDew
GDQuestの教師
フラグメントシェーダー

頂点シェーダーがノードに外側の形状を与えた後、レンダリングパイプラインはその形状内の各ピクセルの色と透明度を決定する必要があります。これを制御するステップがフラグメントシェーダーです。

デフォルトでは、Godotはノードのテクスチャを取得し、そのすべてのピクセルを頂点シェーダーが出力した形状に配置します。しかし、fragment()関数に独自のコードを記述することで、この動作を変更できます。

フラグメントシェーダーはオブジェクトの各ピクセルに対して実行され、そのピクセルが得る色を返します。fragment()という名前の関数内でCOLORに値を割り当てることで、結果の画像を変更または完全に変更できます。例えば、このコードはスプライトを黒い長方形に変えます。

void fragment() {
  COLOR = vec4(0.0, 0.0, 0.0, 1.0);
}

これでフラグメントシェーダーが何をするか分かったので、テストできる最小限のシーンを作成しましょう。

次のセクションでは、Sprite2Dを設定し、カスタムシェーダーを適用して、これを自分で試せるようにします。そこからポータルを構築していきます。

なぜこれは「フラグメント」シェーダーと呼ばれるのに、各ピクセルに対して実行されるのですか?

厳密に言えば、「フラグメントシェーダーは各ピクセルに対して実行される」というのは正確ではありませんが、ほとんどの人が使用する便利な嘘です!

グラフィックスプログラミングでは、フラグメントは候補ピクセルです。レンダリングプロセス中に他のオブジェクトに覆われたり、完全に破棄されたりするため、フラグメントが常に実際のピクセルになるとは限りません。

しかし、日常的なシェーダー作業では、その区別を必要とすることはめったにありません。フラグメントを「現在のピクセル」と考えることで、レンダリングパイプラインの詳細に気を取られることなく、シェーダーの動作に集中できます。したがって、特に言及されない限り、「ピクセル」と「フラグメント」は同じものと見なすことができます。

2. Godotシーンの設定

信じられないかもしれませんが、Godotを開いた瞬間からずっとシェーダーを使用しています。ゲームで見かけるすべてのゲームオブジェクトは、オブジェクトのテクスチャを取得して画面にペイントするデフォルトシェーダーを使用して描画されています。

独自のビジュアルエフェクトを作成するには、ノードのデフォルトシェーダーを置き換えます。

スプライトのデフォルトシェーダーを置き換えるシーンを設定しましょう。新しいGodotプロジェクトで、シーンドックの「シーン」->「新規シーン」をクリックし、「2Dシーン」を選択して、ルートにNode2Dを作成します。

[画像: Scene -> 2D Scene]

次に、Node2Dの子としてSprite2Dノードを追加します。シーンドックの「追加」ボタンをクリックして「新規ノード作成」ダイアログを開き、Sprite2Dを検索します。

[画像: Creating a Sprite2D node]

Sprite2Dノードがシーンドックに表示されますが、現在テクスチャがないため、2Dゲームビューにはまだ表示されません。テクスチャを与えるには、インスペクタードックに移動し、Textureプロパティを見つけます。「クイックロード」アイコンをクリックしてテクスチャ選択ダイアログを開き、デフォルトのGodotアイコンをダブルクリックしてロードします。

[画像: Giving the Sprite2D a texture]

選択した画像がビューポートに表示されます。これが私が導入で話したデフォルトシェーダーの動作です!Godotは、Sprite2Dノードの位置に基づいてスプライトのテクスチャを画面に描画します。

このデフォルトシェーダーをオーバーライドしましょう。

スプライトにシェーダーマテリアルをアタッチする

カスタムシェーダーをスプライトにアタッチするには、まずマテリアルを作成し、次にそのマテリアルにシェーダープログラムを作成してロードする必要があります。この2つの違いは何でしょうか?

  • シェーダーは、スクリプトのように再利用可能なコードのチャンクです。Godotがノードを画面にどのように描画するかを定義します。シェーダーは複数のシーンやマテリアルで再利用できます。
  • マテリアルはそのシェーダーの特定の設定です。例えば、スプライトを色付けできるシェーダーをコーディングしたとします。緑色に色付けするマテリアルと、赤色に色付けする別のマテリアルを持つことができます。どちらのマテリアルも同じシェーダーを使用します。

Sprite2Dノードにマテリアルを追加しましょう。インスペクターに移動し、CanvasItemセクションのMaterialプロパティを見つけます。「空」スロットをクリックし、開いたメニューからShaderMaterialを選択して新しいシェーダーマテリアルを作成します。これにより新しいShaderMaterialリソースが作成され、その中にシェーダーコードを作成またはロードできます。

ShaderMaterialリソースをクリックして展開します。次に、Shaderの隣の「空」スロットをクリックし、New Shader...を選択します。

[画像: Creating a new Shader]

新しいシェーダーの作成

これにより、シェーダー作成ダイアログが開きます。このダイアログでは、デフォルト値をそのままにしておきます。

  • TypeがShaderに設定されている場合、シェーダーコードを入力できる新しいShaderリソースが作成されます。VisualShaderは、シェーダーを設計するためにビジュアルエディターを使用する別のオプションです。
  • ModeがCanvas Itemに設定されている場合、2Dノードで使用できるシェーダーが作成されます。

Pathをres://portal.gdshaderに変更し、「作成」をクリックします。これにより、シェーダーがファイルに保存されます。

[画像: Create Shader Configuration]

シェーダー設定はこのようになるはずです。

Godotは、下部パネルのシェーダーエディターを開き、新しく作成したカスタムシェーダーをデフォルトのテンプレートで埋めます。Godotが作成したvertex()関数とfragment()関数を編集することで、スプライトの描画方法を制御できます。この時点でのシーンは次のようになっているはずです。

[画像: The shader editor open]

シーンはシェーダーコードの実験を開始する準備ができました。コーディングを開始する前に、ポータルの実装をどのように計画するかについて説明します。これにより、何を作成し、複雑なエフェクトを管理可能な部分にどのように分解するかについてのアイデアが得られます。

3. ポータルの実装計画

これは、次のセクションで目指すポータルエフェクトです。これはブラウザでライブ実行されているコピーなので、自由にパラメータを調整してください!これらは、最終的なエフェクトをGodotで構成できる方法の一部です。

Color InnerRGB

Color RimRGB

Breathing Strength
0.03

Displacement Strength
0.05

Noise Speed
0.1

ポータルエフェクト

この種の СЭффект を作り始めるにはどうすればよいですか?

ポータルには2つの色があり、ぐにゃぐにゃと揺れ、時間の経過とともに成長・縮小します。何が起こっているのか理解するのは難しいです。一度にすべてを書き込むことはできるのでしょうか?

必要ありません!

シェーダーの作成は、ゲーム開発の他の部分と同様に、反復的なプロセスです。一度にエフェクトを作成しようとするのではなく、段階的に構築します。プロのテクニカルアーティストでさえ、多くの単純なエフェクトを組み合わせてシェーダーを作成します。

OldDew
GDQuestの教師

各ステップで、同じ基本的なサイクルに従います。

  1. 目標に向かって最小のステップから始めます。
  2. シェーダープログラミングツールキットで、このステップを完了するのに役立つツールを見つけます。
  3. ツールをシェーダーコードで試して、何が起こるかを確認します。

シェーダーが完了するまで、このループを繰り返します。

では、どこから始めましょうか? ポータルをコア要素に分解し、達成可能な目標を探します。もう一度ポータルを見てみましょう。

エフェクトをよく見てください。大まかにどのような形に見えますか?

円! これが開始点となります。まず青い円を描き、そこから進めていきます。

どのようにアニメーションさせたり、変形させたり、明るい外側のリングを作成したりするのでしょうか?

ゲームシステムやメカニクスを構築するのと同じように、シェーダーツールキットを構築し、複雑なエフェクトを管理可能なステップに分解する経験を積む必要があります。私たちはシェーダーの書き方を学ぶためにここにいるので、今回は私がステップを分解します。

テクニカルアーティストは次のようにアプローチします。

  1. ピクセルの距離を測定して、円をプロシージャルに描画する
  2. マスクを使用して、円を外側のリングと内側の円に分割する
  3. マスクのエッジをスムーズにして、プロシージャルに生成されたリングと円の形状の周りにアンチエイリアシングを追加する
  4. 周期的関数と時間を使用して形状をアニメーションさせる
  5. ノイズテクスチャを使用して形状を変形させ、高周波の詳細を追加する

これらのテクニックを一つずつ組み合わせることで、単純なフラットな形状を描画することから、アニメーションするスペースポータルまで進むことができます。

これが私たちが従う計画です。まず、シェーダーのすべてのピクセルを1つの色でペイントします。これにより、ステップごとに変化するエフェクトが表示されます。

OldDew
GDQuestの教師

4. シェーダーコードで最初のピクセルを描画する

最小限のステップから始めましょう。すべてのピクセルを1つの色でペイントします。まずシェーダーを動作させ、その後で構文や仕組みについて詳しく説明します。

まず、色を格納する変数を定義します。シェーダーエディターのfragment()関数に次のコード行を入力します。これは以前見た明るい青色です。

void fragment() {
  vec4 portal_color = vec4(0.23, 0.35, 0.65, 1.0);
}

何も起こりません。なぜでしょう?

変数を1つ作成するだけでは値を格納するだけで、グラフィックカードにその値を使用するように指示するわけではありません。

グラフィックカードに何を描画するかを指示するには、シェーダー言語の特別な組み込み出力変数を使用する必要があります。例えば、色を選択するには、特別なCOLOR変数に色を割り当てます。

fragment()関数の末尾に新しいコード行を追加して、色をCOLOR出力変数に割り当てます。

void fragment() {
  vec4 portal_color = vec4(0.23, 0.35, 0.65, 1.0);

  COLOR = portal_color;
}

スプライトは完全に青くなるはずです。

COLOR出力は、Godotに現在のピクセルに適用する色を伝えます。シェーダーはすべてのピクセルに対して実行されるため、スプライト全体が青くなります。

これで最初のシェーダーをコーディングしました!カラー値について詳しく見る前に、このCOLOR変数を設定するだけの小さな関数が、なぜスプライト全体を同じ色でペイントするのかを見てみましょう。

GDShaderプログラミング言語の構文の仕組み

Godotのシェーダープログラミング言語であるGDShaderは、OpenGL Shading Language(GLSL)に基づいています。これはGLSLの簡略版であり、テクニカルアーティストが既存のシェーダーをGodotに適合させるのを非常に容易にします。

GDShaderの構文は、GDScriptよりもCやC++のような言語に近いです。幸いなことに、GDScriptで知っていることをこのプログラミング言語にマッピングすることで調整できます。最も重要な違いは次のとおりです。

GDScriptはインデントを使用して、関数またはコードブロックの開始と終了を決定します。GDShaderでは、波括弧({})を使用します。開始波括弧が関数またはコードブロックを開始し、終了波括弧がそれを終了します。

void fragment() {
  // これはフラグメントシェーダー関数の本体です。
}

また、波括弧を持つほとんどの言語と同様に、GDShaderのコードブロック内の各行はセミコロン(;)で終わります。

void fragment() {
  vec4 portal_color = vec4(0.23, 0.35, 0.65, 1.0);
  COLOR = portal_color;
}

変数の定義

GDScriptでは、次のように変数を定義できます。

var position: Vector2

varキーワードを使用し、次に変数名、次にオプションで変数型を記述します。デフォルト値を変数に割り当てることもできます。

GDShaderでは、型は必須であり、最初に変数名が続きます。varキーワードは記述しません。これはGDShaderの上記のGDScriptコードと同等です。

vec2 position;

また、GDShaderでは、変数型と値の型は常に完全に一致する必要があります。GDScriptは数値に関して少し寛容ですが、変数がGDShaderでfloat(小数)として宣言されている場合、それに割り当てる値には小数点を付ける必要があります。

float brightness = 0.0;

GDShaderの構文の詳細については、公式Godotドキュメントを参照してください:Godotのシェーディング言語。

5. シェーダーがピクセルを並列で描画する方法

シェーダーがスプライト全体を着色する方法を見る前に、プロセッサで実行されるGDScriptプログラムでそれをどのように行うか考えてみましょう。

以下のコードは、GDScriptで画像全体を青くペイントする方法を示しています。

extends Sprite2D

func _ready():
  var image := Image.create(128, 128, false, Image.FORMAT_RGBA8)

  for y in image.get_height():
    for x in image.get_width():
      image.set_pixel(x, y, Color.BLUE)

  texture = ImageTexture.create_from_image(image)

このコードは以下のことを行います。

  • メモリ内に128x128ピクセルの画像を作成します。
  • 画像の各ピクセルをループして青色にします。
  • 結果の画像をスプライトのテクスチャとして割り当てます。

GDScriptでは、画像内のすべてのピクセルをペイントするために、各ピクセルの座標をループ処理し、個々のピクセルを青色にします。このGDScriptループはプロセッサ上でシーケンシャルに実行されます。

これは、上記のコードが1つのピクセルを処理し、それを青色にし、次に次のピクセルを処理し、それを青色にし、というように進むことを意味します。

[画像: pixels being set on the cpu]

プロセッサがピクセルをシーケンシャルに処理する方法

この動作は、ゲームロジックを作成する際に非常に役立ちます。データ構造とループ処理方法を完全に制御できるからです。しかし、数千または数百万ピクセルで構成される画像をレンダリングするには、このアプローチは遅く、パフォーマンスを大幅に制限します。

ここでGPUが登場します! あらゆるゲームプレイロジックに対応できる非常に賢いシーケンシャルな脳を持つ代わりに、GPUは小さく具体的な問題を並列で解決できる何千もの小さな脳を持っています。どれくらい小さいか?ピクセルサイズです!

シェーダーでは、画像全体を描画するプログラムを記述するのではなく、GPUの各小さな脳が1つのピクセルを描画するために使用するプログラムを記述します。各脳は自身のピクセルを描画することだけを気にするため、他の脳がタスクを完了するのを待つ必要がなく、一度に実行できます。したがって、GPUで多くのピクセルを変更することは、次のように見えます。

[画像: pixels being set on the cpu]

ピクセルはGPU上で並列に処理されます

同じfragment()関数が多くのピクセルに対して並列に実行され、各並列実行はそのピクセル自身の色を生成します。この例では、すべてのピクセルが同じ色を受け取るため、スプライト全体が青くなります。

次に異なる形状を作成するためにコードを変更しますが、その前に、portal_colorを構成する値を見て、結果をどのように変更できるかを確認しましょう。

OldDew
GDQuestの教師

6. カラー値の理解

コンピューターは、赤、緑、青の色とアルファ(透明度)の4つのカラーチャンネルを混合してさまざまな色を表現します(これはRGBカラーまたはRGBAと呼ばれます)。これは、画面上の物理的なピクセルが実際には3つの小さなライトで構成されているためです。1つは赤、1つは緑、もう1つは青です。

[画像: Closeup of pixels]

非常にクローズアップで撮影されたコンピューターディスプレイのピクセル。

Godotシェーダーでは、これらの値は0.0から1.0の範囲です。赤チャンネルが0.0の場合、赤色ライトはオフになります。1.0の場合、赤色ライトは完全にオンになります。

これは、各色を浮動小数点値で表すことができることを意味します。フラグメントシェーダーで各値をfloatとして定義できます。

void fragment() {
  float red = 0.0;
  float green = 0.0;
  float blue = 1.0;
  float alpha = 1.0;
  COLOR = vec4(red, green, blue, alpha);
}

このコードでは:

  • 赤はオフになっています。
  • 緑はオフになっています。
  • 青は完全にオンになっています。
  • アルファは完全にオンになっており、色は完全に不透明であることを意味します。

これは機能しますが、これらの4つの値はすべて単一の色を表すため、それらをまとめておくのが理にかなっています。GDShader言語では、最大4つの数値をベクトルにグループ化できます。

void fragment() {
  vec4 portal_color = vec4(0.0, 0.0, 1.0, 1.0);
  COLOR = portal_color;
}

1.0や0.0のようなきれいな数値は素晴らしい例になりますが、しばしば人工的に見える色を生成します。

これは色にのみ当てはまるわけではありません。ほとんどのエフェクトは、「不完全な」数値でより自然に見えます。

portal_color変数の各カラーチャンネルを試してみてください!青を少し減らし、赤と緑を少し混ぜてみてください。Godotはシェーダーを再コンパイルし、変更を加えるたびにビューポートを更新します。

OldDew
GDQuestの教師

シェーダーのプログラミングを続ける前に、シェーダープログラミング言語のもう1つの重要な機能について説明したいと思います。私たちが使用したこのCOLOR変数は、シェーダー組み込みと呼ばれ、グラフィックカードからコンテキストを取得したり、出力を書き込んだりする方法です。ポータルを完成させるために必要となる、さらに多くの組み込みがあります。

ベクトルの個々の値にアクセスできますか?

はい、GDShaderはベクトルのコンポーネントにアクセスするための2つの同等の方法を提供します。r、g、b、aとx、y、z、wです。

この例では、portal_color.rは色をfloatとして返す赤成分です。portal_color.xと記述した場合も同じことが起こります。

2つの表記法が存在するのは、ベクトルが色と位置の両方を表すためによく使用されるからです。色のxチャンネルにアクセスするのは非常に紛らわしいので、色を扱う場合はrgbaを使用するのが良いでしょう。同様に、位置の場合はxyzwを使用するのが良い習慣です。

単一のコンポーネントに限定されるわけではありません。ベクトルの最も便利な機能の1つはスウィズリングです。これは、複数のコンポーネントを一度に並べ替えたりアクセスしたりすることです。

例えば、portal_color.rgbは赤、緑、青のコンポーネントを含むvecを返しますが、portal_color.bgrは同じコンポーネントを逆順で返します。コンポーネントを繰り返すこともできます。例えば、portal_color.rrです。これは、すべての値が赤成分であるvec2を作成します。

注意すべき重要な点は、コンポーネント間で異なる表記法を使用してスウィズリングすることはできないということです。例えば、portal_color.xryaは位置(x、y)と色(r、a)の両方の表記法を使用しているため無効です。

なぜこのデータ型はcolorではなくvec4と呼ばれるのですか?

出力またはプログラムのパフォーマンスに対してより多くの制御を提供するプログラミング言語では、データ型は通常、コンピューターのメモリに実際に格納されているものに基づいて名前が付けられます。

見たように、グラフィックカード上では、色は4つの数値のシーケンスです。数学では、ベクトルは数値のシーケンスです。ベクトルは2次元または3次元に限定されず、画面上またはグリッド上の座標だけでなく、それ以上のものを表すことができます。例えば、色です!

そのため、データ型はvec4と呼ばれます。これは4つの数値のシーケンスです。

7. シェーダー組み込み

組み込み変数は、あなたとGPUの間の橋渡しとなります。

一部はGPUからコンテキストを取得できます。例えば:

  • TIME: エンジン開始からの時間
  • VERTEX: 現在の頂点の位置
  • UV: 描画領域に対する現在のピクセルの座標

その他はGPUに指示を送信できます。

  • COLOR: 現在のピクセルに適用する色をGPUに指示します(fragment()関数で使用する場合)。
  • VERTEX: 現在の頂点の新しい位置をGPUに指示します(vertex()関数で使用する場合のみ)。

各組み込みは常に処理中の現在のピクセル、頂点、またはデータ片を参照することを心に留めておいてください。この小さな詳細は、シェーダープログラミングのアプローチ方法において大きな違いを生みます。

OldDew
GDQuestの教師

VERTEXはなぜGPUにコンテキストを取得したり提供したりできるのですか?

一部の組み込みは、変数をどこで使用するかによって、GPUから情報を取得したり、GPUに情報を提供したりできます。頂点シェーダー(つまり、vertex()関数でコードを記述する場合)では、VERTEX変数を読み取ったり書き込んだりして、処理中の頂点の位置を使用または変更できます。

TIME変数のように、読み取りのみが可能な組み込みもあります。

フラグメントシェーダーの組み込み変数とその動作のリストを確認するには、公式ドキュメントを参照してください。

in、out、inout修飾子に注意してください。組み込みが読み取り専用か、書き込み専用か、または両方の操作を許可するかを示します。

利用可能な組み込み変数とその動作は、頂点シェーダーとフラグメントシェーダーの間で異なります!

シェーダーを使い始めた際の一般的な間違いは、間違ったシェーダー関数で組み込み変数を使用することです。一部の組み込みは一方の関数でしか利用できないことに注意する必要があります。さらに悪いことに、両方のシェーダー関数で使用できるものもありますが、それらの意味は使用する場所によって異なります!

VERTEX組み込み変数を例にとってみましょう。

  • 頂点シェーダー内で使用する場合、スプライトの中心に対する現在の頂点の位置を参照します。
  • フラグメントシェーダーで使用する場合、画面上のピクセルの位置です。

これはかなり混乱します!ここで重要なのは、VERTEXの特定の動作を暗記することではなく、公式ドキュメントを読む際に、組み込み変数が属するシェーダーのステージを認識することです。

これで、シェーダーがすべてのピクセルを着色する方法を理解したので、ポータルの形状を作成することに進むことができます。念のため、最終結果は次のようになります。

青いポータル

揺れや歪みを無視すると、ポータルの全体的な形状は円のように見えます。

しかし、興味深い問題があります。同じフラグメント関数が各ピクセルに対して並列に実行されることを学びました。すべてのピクセルがまったく同じコードを実行する場合、GPUに円を描画するように指示するにはどうすればよいでしょうか?

一部のピクセルを円の一部にし、他のピクセルは透明のままにする必要があります。

コードは各ピクセルで同じですが、入力値は異なります。 ピクセルが円の一部であるかどうかを決定するには、そのピクセルがどこにあるかを知るだけで十分です。ピクセルの位置に応じて、描画するかしないかを決定します。これがシェーダーでプロシージャルな形状を描画する方法です!

Godotは、UV組み込み変数を通じてこの情報を提供します。

8. UV組み込み

UV組み込みは、現在のピクセルの位置を正規化座標のペアとして格納するvec2変数です。これは、水平および垂直の両方のコンポーネントが0.0から1.0の範囲であり、vec2(0.0, 0.0)がスプライトの左上隅、vec2(1.0, 1.0)が右下隅であることを意味します。

そのため、ノードの実際のサイズは関係ありません。UV座標の(0.2, 0.5)は、スプライトが小さな64x64アイコンであっても、巨大な1024x1024画像であっても、常に「左から20%、上から50%」を意味します。

しかし、任意の座標にあるピクセルをどのように知ることができるでしょうか?シェーダーは各ピクセルに対して並列に実行されるため、print()関数のように値を印刷することはできません。それはコンソールを何千ものメッセージで溢れさせるでしょう。代わりに、値を色として視覚化します。

UVベクトルの水平座標を取得し、COLOR組み込みの赤チャンネルに割り当てて、黒から赤までのグラデーションを取得します。fragment()関数をこのコードに置き換えてください。

void fragment() {
  COLOR = vec4(UV.x, 0.0, 0.0, 1.0);
}

この視覚化は、x座標に関する興味深い事実を明らかにします。グラデーションの左側は黒です。色を追加しない場合、シェーダーの左側でUV.xは0.0です。一方、右側は明るい赤です。これはUV.xが最大値1.0に達する場所です。

この実験を繰り返しましょう。今回は、垂直成分を緑チャンネルに割り当てます。fragment()関数を次のように更新してください。

void fragment() {
  COLOR = vec4(0.0, UV.y, 0.0, 1.0);
}

グラデーションはシェーダーの上部で黒くなっています。これは、UV.yが上部で0.0であり、下部に向かって1.0に増加することを示しています。GDShaderでは、2Dシーンと同様に、UV組み込みのY座標は下に行くにつれて増加します。

次に、UVベクトルの両方の成分を同時に組み合わせて視覚化しましょう。UVベクトルのxおよびy成分の両方を視覚化するようにコードを編集します。

void fragment() {
  COLOR = vec4(UV.x, UV.y, 0.0, 1.0);
}

色の配置方法を見ると、UVのプロパティを次のように解釈できます。

  • 左上は黒です。UV.xとUV.yの両方が0.0だからです。
  • 右上は赤です。UV.xが1.0に達し、UV.yはまだ0.0だからです。
  • 左下は緑です。UV.yが1.0に達し、UV.xはまだ0.0だからです。
  • 右下は黄色です。UV.xとUV.yの両方が1.0に達します。

しかし、UVについて覚えておくべき最も重要なことは次のとおりです。

  • UVは、スプライトの境界ボックスに対するピクセルの位置を示す正規化座標です。
  • UV.xは左から右に増加します。
  • UV.yは上から下に増加します。
  • UVの原点座標vec2(0.0, 0.0)は、スプライトの境界ボックスの左上隅にあります。
  • UV座標vec2(1.0, 1.0)は、スプライトの境界ボックスの右下隅にあります。

値を色として視覚化するという考え方は、UV座標に限定されません。これは、シェーダーを検査およびデバッグするために使用する主なツールの1つです。

GDScriptでは、print()関数またはデバッガーを使用します。シェーダーでは、出力を色としてプレビューし、解釈します。

組み込み変数、関数、または数学的式の結果を理解しようとするたびに、それをまず色に変換してみてください。予期しないパターンを明らかにすることがよくあります。

次に、UV座標を使用して円を描画しましょう。

9. 円を描画する

ピクセルが円の内側にあるためには、中心からの距離がその円の半径より小さくなければなりません。

このルールをすべてのピクセルに適用すると、円全体を着色することになります。

作成したいポータルはシェーダー領域のちょうど中央にあります。UV座標は0.0から1.0の範囲であるため、中央は座標vec2(0.5, 0.5)を持つ点になります。

組み込みシェーダー関数であるdistance()関数を使用して、各ピクセルとその点との距離を計算できます。これは2点間の距離を返します。例えば、distance(vec2(0.0, 0.0), vec2(1.0, 0.0))は1.0を返します。

中心点vec2(0.5, 0.5)と現在のUV座標との距離を計算できます。その距離がターゲット半径より小さい場合、ピクセルは円の内側にあります。それ以外の場合は、円の外側にあります。

距離の視覚化

比較を行う前に、distance()の呼び出し結果を視覚化しましょう。結果をdistance_from_centerというfloat変数に保存し、最終的なカラーチャンネルのいずれかに配置します。

以下のコード例では、distance_from_centerをすべてのRGBチャンネルに割り当てました。そうすると、目に優しいグレースケール画像が生成されます。シェーダーファイルでfragment()関数を次のように更新して結果を視覚化してください。

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);

  COLOR = vec4(
    distance_from_center,
    distance_from_center,
    distance_from_center,
    1.0
  );
}

何が見えますか?そして、その出力をどのように解釈しますか?自分で観察してみてください。私の解釈を読み進めてください。

画像の中心は黒く、エッジやコーナーに向かうにつれて、色は徐々に明るくなり、明るい灰色までになります。

中心に近いピクセルは、それらのUV座標とvec2(0.5, 0.5)点との距離が0.0に近いため、色を追加しません。エッジに近づくにつれて距離が増加するため、値が高くなり、ピクセルが明るくなる理由です。

距離グラデーションを円に変える

距離グラデーションをはっきりした円に変えるには、決定が必要です。任意の半径値(例えば0.1)を選択し、floatに保存して、中心からの距離と比較します。

  • それが半径より小さい場合、ピクセルは円の内側にあります。そのためCOLORは青に設定されます。
  • それ以外の場合、ピクセルは不可視です。これは、そのアルファ値が0.0であることを意味します。

シェーダーコードを更新しましょう。portal_color変数を再導入し、円の望ましい半径を表す新しい変数を追加し、ifとelse条件を使用して一部のピクセルを青く描画し、他のピクセルを透明のままにします。これらの変数を使用するようにfragment()関数を更新してください。

void fragment() {
  vec4 portal_color = vec4(0.23, 0.35, 0.65, 1.0);

  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);

  float radius = 0.1;
  if (distance_from_center < radius) {
    COLOR = portal_color;
  } else {
    COLOR = vec4(0.0);
  }
}

背景がチェック柄なのはなぜですか?

Godotや他の多くのグラフィカルアプリケーションは、透明度をチェック柄のテクスチャで表現します。

これにより、透明な領域を固体色と区別しやすくなり、画像の一部と間違われるのを防ぎます。

この例では、以前と同様に中心からの距離を計算しています。中心からの距離がradius値より小さい場合、出力COLORをポータル色に設定します。それ以外の場合、完全に透明な色に設定します。これによりピクセルが透明になります。

ポータル半径の値をより良い値に選択できたように思えます。現在の値0.1は小さすぎます。

コードに戻って毎回置き換えるのではなく、別のサイズを試したい場合は、シェーダーユニフォームの助けを借りて、半径をリアルタイムでゲームスクリプトから変更できるようにしましょう。

コード参照:res://portal.gdshader

この時点でのポータルシェーダーはこのようになっているはずです。

shader_type canvas_item;

void fragment() {
  vec4 portal_color = vec4(0.23, 0.35, 0.65, 1.0);

  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);

  float radius = 0.1;
  if (distance_from_center < radius) {
    COLOR = portal_color;
  } else {
    COLOR = vec4(0.0);
  }
}

GDShader制御フロー構文

他のほとんどの言語と同様に、GDShaderはif文、forループ、whileループなどの制御フロー要素をサポートしています。

GDScriptから来た場合、最も大きな構文の違いは、この言語のすべてのコードブロックと同様に、制御フローブロックがインデントによって定義されるのではなく、波括弧({})で囲まれていることです。

If文は次のようになります。

if (condition) {
  // ...
} else if (other_condition) {
  // ...
} else {
  // ...
}

0から10までのforループは次のように記述されます。

for (int i = 0; i < 10; i++) {
  // ...
}

その構造は3つの領域で構成されています。

  • 初期化: int i = 0は整数iの開始値を0に設定します。
  • 比較: i < 10は各ループイテレーションでiの値と10を比較します。条件が偽になると、ループの実行は停止します。
  • インクリメント: i++は各ループイテレーション後にiに1を加算します。

最後に、whileループは次のようになります。

while (condition) {
  // ...
}

組み込み関数

組み込み変数に加えて、Godotは数学やテクスチャ関連のユーティリティなど、あらゆる目的の多くの組み込み関数も提供しています。

中心点間の距離を計算するdistance()組み込み関数を使用しましたが、他にも組み込み関数があります。完全なリストはGodotドキュメントで確認できます。

vec4は4つのパラメータを取らないのですか?

通常はそうです。vec4は4つの値を格納するため、通常は次のように記述します。

vec4(1.0, 0.0, 0.0, 1.0)

利便性のために、Godotは少ない値でベクトルを構築できます。単一の数値をvec4に渡すと、その値がすべてのコンポーネントにコピーされます。したがって、次の2行のコードは厳密に同等です。

vec4(0.0, 0.0, 0.0, 0.0)
vec4(0.0)

このシェーダーでは、vec4(0.0)を使用して完全に透明な黒いピクセルを作成します。アルファコンポーネントは0.0なので、他の値は不可視になるため、RGBコンポーネントの値は関係ありません。

シェーダーでif文を使用するのは悪いことではありませんか?

シェーダーについて少し知っているなら、パフォーマンスを向上させるためにシェーダー内で条件を使用すべきではないと聞いたことがあるかもしれません。

過去にはそうだったかもしれませんが、今は問題ありません。単純なif文とユニフォーム変数をチェックする条件(後で確認します)は、一般的に安全に使用できます。コンパイルされたシェーダーコードが分岐構造を生成するケースでも、条件がコードの大部分をスキップする場合は価値があることがあります。

したがって、シェーダーで常にif文を避けるべきではありません。パフォーマンスに関連するすべてのことと同様に、コードのさまざまなバージョンのパフォーマンスを測定したり、コンパイルされたシェーダーコードを読んだりして、誤った仮定を避けるのが良い考えです。

GPUの条件については、ベテランのグラフィックスプログラマーであるInigo Quilezの記事GPU conditionalsで詳しく読むことができます。

10. ユニフォームによる編集可能なプロパティ

ユニフォームは、シェーダーの外部から値を取得する変数です。GDScriptのエクスポート変数と非常によく似ており、インスペクターに表示され、コードやアニメーションから変更することもできます。

[画像: Increasing the radius uniform in the editor]

これは、エディターから半径を制御できるユニフォームの例です。

ユニフォームは、関数の外にあるシェーダーの先頭近くに宣言します。構文は次のようになります。

uniform float variable_name;

まず、uniformキーワード、次に型(この場合はfloat)、最後に変数名(variable_name)を入力します。

これまでは、円の半径を0.1にハードコーディングしていましたが、これは変更するにはシェーダーを編集する必要があることを意味します。この定数をユニフォームに置き換えて、インスペクターで直接さまざまな値を試せるようにしましょう。シェーダーコードを次のように更新してください。

shader_type canvas_item;

uniform float portal_radius;


void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);

  float radius = 0.1;
  if (distance_from_center < radius) {
  if (distance_from_center < portal_radius) {
    COLOR = vec4(0.23, 0.35, 0.65, 1.0);
  } else {
    COLOR = vec4(0.0);
  }
}

Godotは自動的に、マテリアルマテリアルシェーダーパラメータの下にシェーダープロパティをインスペクターに追加します。プロパティをクリックしてドラッグして、ポータルの半径を増やします。

[画像: Increasing the radius uniform in the editor]

半径ユニフォームの増加

ライブ編集はすでに非常に便利ですが、まだいくつかの粗いエッジがあります。

まず、ポータルが非表示で半径が0.0から始まるのは奇妙です。

次に、インスペクターは任意の浮動小数点値を受け入れますが、0.5より大きい値はポータルをスプライトの境界ボックスの外に押し始めます。

ユニフォームを定義する際にデフォルト値を割り当てることで、より適切な開始値を指定できます。

また、意図された値の範囲を伝えるためにユニフォームヒントを使用することもできます。portal_radiusユニフォーム定義を次のように編集します。

uniform float portal_radius: hint_range(0.0, 0.5) = 0.2;

hint_rangeヒントは、意図された値の範囲をインスペクターに伝えます。これにより、値が0.0と0.5の間に制限され、間隔のエッジ間のスライダーが作成されます。そして割り当てられた数値0.2は、ユニフォームのデフォルト値です。

このガイド全体を通してのインタラクティブな例では、シェーダーユニフォームをスライダーコントロールとして表示しているため、ブラウザで直接シェーダーパラメータを試すことができます。

ガイドを最大限に活用するには、シェーダーコードをコピー&ペーストするだけでなく、Godotエディターで自分で入力してください。

Portal Radiusスライダーを動かして、円が成長したり縮小したりするのを見てください。

Portal Radius
0.2

コード参照:res://portal.gdshader

ここまでの完全なポータルシェーダーコードは次のとおりです。

shader_type canvas_item;

uniform float portal_radius: hint_range(0.0, 0.5) = 0.2;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);

  if (distance_from_center < portal_radius) {
    COLOR = vec4(0.23, 0.35, 0.65, 1.0);
  } else {
    COLOR = vec4(0.0);
  }
}

移動する前に、あなたがすでに学んだことに感謝してください。

  • シェーダーは並列に実行されます。1つのピクセルのプログラムを記述すると、GPUはそれをすべてのピクセルで一度に実行します。
  • COLOR組み込みにより、ピクセルがどのような色になるかを決定できます。
  • UV組み込みはピクセルの座標を提供します。
  • distance()関数は2点間の距離を教えてくれます。
  • シェーダーコードを検査およびデバッグするために、値を色として視覚化します。
  • ユニフォームにより、シェーダーコードを変更せずにシェーダーパラメータを制御できます。

これは、円を描いただけにしては驚くほど長いリストですが、ここから2Dシェーダーエフェクトを作成するために使用する基本的なテクニックをすでにいくつか学びました。

このガイドの残りの部分では、これらの基盤の上に構築する方法を示します。これらの同じアイデアをさまざまな方法で使い続け、新しい問題に遭遇するたびに、Godotのシェーダーツールキットで別のツールを探します。

OldDew
GDQuestの教師

私たちの円は現在、ギザギザの輪郭を持っています。これはエイリアシングと呼ばれます。これはディスプレイによっては微妙または見えにくいかもしれませんが、避けたいものです。一般的に、エッジとエフェクト間の遷移がどれだけスムーズになるかを制御したいと考えています。

スムーズ化のための良いツールである、一般的に使用されるシェーダー関数があります。それはsmoothstep()関数です。次にこれについて学びます。

私の円が潰れています

プロジェクトで円が潰れている場合、Sprite2Dノードが完全な正方形ではないことを意味します。例えば、同じシェーダーがサイズ400x200pxのスプライトでどのように動作するかは次のとおりです。

Portal Radius
0.2

この動作はUVの動作によって駆動されます。UV.xとUV.yは両方とも0.0から1.0の範囲です。つまり、両方ともそれぞれの軸のパーセンテージを表します。

400x200のキャンバスでは、x軸に沿った0.1の距離は40ピクセルを表しますが、y軸に沿った同じ距離はわずか20ピクセルです。両方の軸に同じ値を使用しているため、円は水平方向に2倍のピクセルをカバーし、引き伸ばされているように見えます。

正方形でないキャンバスに円を描画するには、アスペクト比を考慮して、UV値がピクセル単位で等しい距離に対応するようにする必要があります。

shader_typeキーワードは何のためにありますか?

まだ言及していませんが、おそらくシェーダーの最初の行がshader_typeキーワードを使用していることに気づいたでしょう。

shader_type canvas_item;

これは、シェーダーがSprite2DやTextureRectのような2Dノードに使用されることをGodotに伝えます。Canvas Itemは、エンジンで2D描画を行うすべてのノードの基本タイプです。

シェーダーを作成したとき、Godotは作成ダイアログで選択したModeに基づいてこの行を挿入しました。

[画像: Create Shader Configuration]

別の目的でシェーダーを使用したい場合は、別のシェーダータイプを使用する必要があります。最も一般的な4つのタイプは次のとおりです。

  • spatial: 3Dオブジェクト用
  • particles: パーティクルシステム用
  • sky: スカイレンダリング用
  • fog: ボリュームフォグエフェクト用

他にユニフォームヒントはありますか?

はい、色、列挙型、整数、テクスチャ用のヒントもあります。公式ドキュメントで利用可能なすべてのヒントを見つけることができます:GDShader Uniform hints。

2つの注目すべきヒントがあります。

  • source_colorはvec3およびvec4変数用です。インスペクターにカラーピッカーを表示するように指示します。
  • hint_enum("String1", "String2")はint変数用です。エディターにドロップダウンウィジェットを表示します。GDScriptの列挙型のように機能します。String1を選択すると値は0になり、String2を選択すると値は1になります。これを使用してシェーダーの異なるモードを定義できます。

円の半径に使用したヒントhint_range(min, max, [, step])はfloat変数に限定されません。int変数にも機能します。

11. smoothstepでより滑らかな形状を描画する

円をよく見ると、エッジが粗いことに気づくでしょう。これは、ピクセルが青色か完全に不可視かのどちらかであるためです。シェーダーは、円と背景の間のよりスムーズな遷移を作成するための、中間値を生成しません。

この問題を解決するために、GDShaderはsmoothstep()関数を提供します。次のように使用します:smoothstep(edge0, edge1, x)。edge0がedge1より小さい場合、次の値を返します。

  • xがedge0より小さい場合、0.0。
  • xがedge1より大きい場合、1.0。
  • xが両方のエッジの間にある場合、スムーズな中間値。

数値で理解するのは難しいです。関数の出力を視覚化する方がはるかに良いので、そうしましょう。

以下のシェーダー例は、座標UV.xに対してsmoothstep()関数を実行します。ユニフォームスライダーを動かして、smoothstep()関数がどのように機能するかを把握してください。

Edge0
0.3

Edge1
0.7

shader_type canvas_item;

uniform float edge0 : hint_range(0.0, 1.0) = 0.3;
uniform float edge1 : hint_range(0.0, 1.0) = 0.7;

void fragment() {
  float result = smoothstep(edge0, edge1, UV.x);
  COLOR = vec4(vec3(result), 1.0);
}

何を観察しますか?Edge0とEdge1が離れているほど、より広いグラデーションが得られます。この範囲外では、純粋な白または純粋な黒が得られます。

2つのエッジを非常に近づけると、細いスムーズな境界線が得られます。次に、水平座標UV.xに対してsmoothstep()を実行する代わりに、ポータルを描画するために使用したdistance()関数に適用してみましょう。

シェーダーコードを次のバージョンに更新してください。単一の半径ではなく、2つのユニフォーム値とsmoothstep()関数を使用します。これにより、条件ブロックが不要になります。

Edge0
0.4

Edge1
0.41

shader_type canvas_item;

uniform float edge0: hint_range(0.0, 0.5) = 0.4;
uniform float edge1: hint_range(0.0, 0.5) = 0.41;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  float portal = smoothstep(
    edge0, 
    edge1, 
    distance_from_center
  );

  COLOR = vec4(vec3(portal), 1.0);
}

新しい変数をポータルと名付けたのはなぜですか?

シェーダーコードは並列で実行されるため、変数を生成する形状の名前にちなんで名付けることがよくあります。

このシェーダーでは、ポータルを描画することを目指しているため、現在の形状が円であっても、変数をportalと名付けます。

コードをGodotのシェーダーに適用すると、黒と白が表示されるはずです。これは出力のアイデアを与えますが、生成している値は色を表す必要はありません。

任意のチャンネルで使用できます。例えば、透明度を制御するために使用できます。ポータルの青色を一定に保ちながら、smoothstep()を透明度マスクとして使用できます。結果をRGBカラーチャンネルに入れる代わりに、ポータルの形状を最終的な色のアルファチャンネルに適用します。シェーダーコードを更新して、ポータル形状を最終的な色のアルファチャンネルに使用してください。

Edge0
0.4

Edge1
0.41

shader_type canvas_item;

uniform float edge0: hint_range(0.0, 0.5) = 0.4;
uniform float edge1: hint_range(0.0, 0.5) = 0.41;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  float portal = smoothstep(
    edge0, 
    edge1, 
    distance_from_center
  );

  COLOR = vec4(vec3(portal), 1.0);
  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, portal);
}

これで、青い円がスプライトの中央に表示されますが、中心は空で、スプライトの外側のエッジが青くなっています。

これは、smoothstep()が円の内側のピクセルに対して0.0を、円の外側のピクセルに対して1.0を返すためです。その値をアルファチャンネルに使用しているため、それらのピクセルは完全に透明になります。これを修正するには2つの方法があります。

引き算でマスクを反転する

1つの解決策は、smoothstep()の呼び出し結果を1.0から引くことです。これにより、全体の結果が反転します。例えば、0.0を引くと1.0 - 0.0 = 1.0が得られます。1.0を引くと1.0 - 1.0 = 0.0が得られます。シェーダーでは、透明度マスクを反転するために、このような値をよく反転させます。

円の形状を計算する行を次のように更新してみてください。

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  float portal = smoothstep(
    edge0, 
    edge1, 
    distance_from_center
  );

  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, portal);
}

これを次のように変更します。

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  float portal = 1.0 - smoothstep(
    edge0, 
    edge1, 
    distance_from_center
  );

  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, portal);
}

これにより、中心に青い円が復元されます。これはシェーダーで非常によく使われるトリックです。

関数パラメータを反転する

ただし、この場合、引き算でマスクを反転する必要はありません。コードの変更を元に戻してください。

次に、まだ行っていない場合は、Edge0スライダーをEdge1を越えてドラッグしてみてください。何が起こるか見てみましょう。

Edge0
0.4

Edge1
0.41

uniform float edge0: hint_range(0.0, 0.5) = 0.4;
uniform float edge1: hint_range(0.0, 0.5) = 0.41;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  float portal = 1.0 - smoothstep(
    edge0, 
    edge1, 
    distance_from_center
  );

  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, portal);
}

形状が反転したことに気づきましたか?smoothstep()はedge0がedge1より小さいことを要求しないようです。2つのエッジを入れ替えると結果が反転します。これはまさに私たちが求めていることです。関数引数を次のように入れ替えることもできます。

float portal = smoothstep(edge1, edge0, distance_from_center);

新しい関数を学ぶときは、それが「正しい」と思われる範囲を超えてパラメータを試すことを恐れないでください。予期しない動作は、関数が実際に行っていることを理解するための最も速い方法であることがよくあります。

OldDew
GDQuestの教師

関数引数や値を入れ替えるのは未定義動作ではありませんか?

smoothstep()関数の引数を入れ替えることは未定義動作ではありません。その背後にある数学は、パラメータを反転させることをサポートしています。

より一般的には、シェーダー関数を試すことを心配しないでください。シェーダーは、ゼロ除算のような無効な計算でもクラッシュしません。ゼロ除算を行うシェーダーの例を次に示します(このガイドの他の例と同様に、ブラウザでライブ実行される実際のシェーダーです)。

このコードはクラッシュせずに実行されます。

void fragment() {
  COLOR.rgb = vec3(1.0 / 0.0);
}

不正なシェーダーコードは望ましくない視覚的なグリッチを引き起こす可能性がありますが、一般的にはクラッシュしません。GodotやGDScriptと同様に、シェーダーコードは無効な操作を適切に処理し、グラフィックカード上でプログラムを実行し続けるように努めます。

したがって、コードのエラーやクラッシュのリスクを冒して、数値や操作を試すことを心配する必要はありません。

左側のポータルの外観と、ソフトエッジを追加した後の外観を比較してください。画面の解像度によっては、違いが分かりにくい場合があります。左側のバージョンでギザギザのエッジを探してください。

コントロールの改善

円はスムーズに見えますが、コントロールは改善される可能性があります。以前は、半径を増やしたい場合、単一の半径値を調整するだけでよかったのです。今では、2つのエッジを変更するだけでなく、ポータルの柔らかさを同じに保つために値の間隔を把握する必要があります。

これを実装するより良い方法は、それらを互いに関連付けることです。radius値とsoftnessユニフォームを追加します。コードを更新してradiusユニフォームを再導入し、新しいsoftnessユニフォームを追加します。

Radius
0.4

Softness
0.01

uniform float edge0: hint_range(0.0, 0.5) = 0.41;
uniform float edge1: hint_range(0.0, 0.5) = 0.40;
uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  
  float portal = smoothstep(
    edge0, 
    edge1, 
    radius, 
    radius - softness, 
    distance_from_center
  );
  
  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, portal);
}

これは次のように変更されます。

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  float portal = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, portal);
}

このように、ポータルを制御するのがはるかに簡単になります。以前は、円のサイズを変更したい場合、2つの変数を変更する必要がありました。

今では、内側のエッジがradius - softnessになりました。これにより、常に外側の半径から固定された距離を保ちます。ポータルのサイズや柔らかさを変更する際に、2つの半径を気にする必要がなくなりました。

これでスムーズな円を描画できるようになったので、次の目標であるポータルに外側のリングを付ける準備ができました。

コード参照:res://portal.gdshader

ここまでの完全なポータルシェーダーコードは次のとおりです。

shader_type canvas_item;

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  float portal = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, portal);
}

12. マスクでリングを作成する

もう一度達成したいエフェクトを見てみましょう。

青いポータル

ポータルは、暗い中央と、世界の外側を隔てるリング状の縁という、2つの主要な部分で構成されています。

新しい形状をゼロから作成する代わりに、円形の形状を再利用して、そこから小さな円をくり抜くことができます。画像の特定の部分を隠すプロセスはマスキングと呼ばれます。

まず、元の円を取得し、その半径ユニフォームをradius_outerにリネームします。

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float radius_outer: hint_range(0.0, 0.5) = 0.4;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);

  float portal = smoothstep(
    radius,
    radius - softness,
  float mask_outer = smoothstep(
    radius_outer,
    radius_outer - softness,
    distance_from_center
  );

  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, portal);
  COLOR = vec4(vec3(mask_outer), 1.0);
}

このコードでは、マスクをプレビューするために一時的にportal_color変数を使用するのをやめていることに注意してください。これはシェーダーを実験する伝統的な方法です。変更を視覚化するために、出力COLOR値を変更します。

次に、radius_innerユニフォームと2番目のsmoothstep()呼び出しを追加して、より小さな円を生成します。呼び出しは外側の円と同じですが、新しい、より小さな半径を使用します。

uniform float radius_outer: hint_range(0.0, 0.5) = 0.4;
uniform float radius_inner: hint_range(0.0, 0.5) = 0.3;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);

  float mask_outer = smoothstep(
    radius_outer,
    radius_outer - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius_inner,
    radius_inner - softness,
    distance_from_center
  );

  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(vec3(mask_outer), 1.0);
  COLOR = vec4(vec3(mask_inner), 1.0);
}

私たちのコードは現在2つの円、より大きい円とより小さい円を生成します。リング形状を作成するために、内側の円マスクをより大きい円マスクから引くことができます。リムのマスクを計算するために新しい変数を作成します。

Radius Outer
0.4

Radius Inner
0.3

uniform float radius_outer: hint_range(0.0, 0.5) = 0.4;
uniform float radius_inner: hint_range(0.0, 0.5) = 0.3;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);

  float mask_outer = smoothstep(
    radius_outer,
    radius_outer - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius_inner,
    radius_inner - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(vec3(mask_inner), 1.0);
  COLOR = vec4(vec3(mask_rim), 1.0);
}

これはなぜ機能するのですか?

覚えていますか:smoothstep()は0.0、1.0、またはその中間の値を返します。

2つのマスクが同じ値を持つ場所では、一方から他方を引くと0.0になります。小さい円は大きい円の完全に内側にあるため、小さい円を大きい円から引くと中央領域が0.0になります。これにより、リムのみが表示されます。

マスクの作成と組み合わせを学びました!

マスクとは、現在のピクセルでエフェクトがどれだけ強く適用されるかを示す数値です。

マスク値が0.0は「エフェクトを適用しない」ことを意味します。1.0は「完全に適用する」ことを意味し、その間の値はスムーズな遷移を作成します。

この例のマスクはリングを生成しますが、必要に応じていくつのマスクでも重ねて組み合わせることができ、複雑な形状を作成できます。

マスクは形状の描画に限定されないことに注意してください。事実上あらゆるエフェクトを制御できます。例えば:

  • テクスチャの一部を表示または非表示にする
  • 色をブレンドする
  • 特定の領域にのみエフェクトを適用する

このガイドの後半で、色をブレンドしたり、特定領域にのみエフェクトを適用したりするためにマスクを使用します。

OldDew
GDQuestの教師
マスクの着色

私たちは1つのマスクだけをカラー変数で視覚化していますが、実際には完全な円、大きい円、小さい円、そしてリムのマスクがあります。それらすべてをどのように色付けするのでしょうか?さて、まずリムのマスクを使用して色を表示しましょう。

どうやって?portal_colorとリムのマスクを一緒に使用します!この「白黒」のリムマスクをアルファチャンネルとして使用して、リムのみを描画し、黒いピクセルを破棄します。

リムマスクを使用して色を表示するようにCOLOR出力を編集します。

void fragment() {
  // ...

  float mask_rim = mask_outer - mask_inner;

  COLOR = vec4(vec3(mask_rim), 1.0);
  COLOR = vec4(portal_color, mask_rim);
}

これで、シェーダーは実験で見たリングを描画しますが、ポータルの色とユニフォームを使用しています。

Radius Outer
0.4

Radius Inner
0.3

Softness
0.01

これはシェーダーコードです。

shader_type canvas_item;

uniform float radius_outer: hint_range(0.0, 0.5) = 0.4;
uniform float radius_inner: hint_range(0.0, 0.5) = 0.3;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);

  float mask_outer = smoothstep(
    radius_outer,
    radius_outer - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius_inner,
    radius_inner - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, mask_rim);
}

これは、シェーダーでリング形状を描画する1つの方法です。2つの円を減算します。一般的に、同じ形状の2つのマスクを減算して、プロシージャルなストロークまたはアウトラインを生成できます。

2番目の円を追加したときに、使用するのがそれほど直感的ではないユニフォームを導入しました。リングを形成するには2つのスライダーを動かす必要があり、内側の半径が外側の半径より大きい場合、出力は表示されなくなります。移動する前にユニフォームを簡略化しましょう。

ユニフォームの簡略化

ポータルで制御したいものは何でしょうか?リムの厚さと全体的な半径だけです。ユニフォームを変更して、リムを全体的な厚さの観点から表現できます。

radius_innerユニフォームを新しいrim_widthユニフォームに置き換えて、計算を更新します。その間、全体的な形状の半径を単にradiusと呼ぶように戻しましょう。

Radius
0.4

Rim Width
0.05

Softness
0.01

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;

uniform float softness: hint_range(0.0, 0.2) = 0.01;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  
  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;
  
  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, mask_rim);
}

シェーダーは、それをビルディングブロックに絞り込むことで作成します。しかし、それらのブロックが配置されたら、それらを意味のあるコンポーネントに組み合わせるのが良い習慣です。

あなたの最終的なポータルは、円、ベクトル、および任意の数値のコレクションではなくなるはずです。代わりに、リム、色、内部、またはサイズのようなプロパティを持つ実際のオブジェクトのように動作するはずです。これらは後で調整したいコントロールであり、他の人がすぐに理解できるものです。

あまり早く行わないでください!

名前を付けたり、シェーダーユニフォームを正確にしたりすることに集中すると、目の前の問題から気をそらす可能性があります。実験中に意味のない形状を使用することを自分に許可してください。円を2つ使用したときのように。この段階では、目標はエフェクトを機能させることであり、良いパラメータを設計することではありません。

結果に満足したら、クリーンアップのために2回目のパスを取ります。変数をリネームし、関連する概念をグループ化し、実装方法ではなくエフェクトを説明するコントロールを公開します。シェーダーが成長するにつれて、これらのコントロールの一部を再度確認することになるかもしれません。

シェーダー開発は反復的です。良いインターフェースを設計することは、さらに別の反復です。

OldDew
GDQuestの教師

コード参照:res://portal.gdshader

この時点でのシェーダーは次のコードになっているはずです。

shader_type canvas_item;

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 portal_color = vec3(0.23, 0.35, 0.65);
  COLOR = vec4(portal_color, mask_rim);
}

13. ポータルを塗りつぶす

これまでは、単一の色とマスクのみを扱ってきました。しかし、ポータルが別の場所への入り口であるという錯覚を売るためには、それぞれ独自の見た目を持つ、より明るい縁とより暗い中央の両方が必要です。

しかし、複数の形状と色を同時に正確に使用するにはどうすればよいでしょうか?

答えは驚くほど簡単です。生成した各マスクを異なる色に適用して、形状をスタックできます。リムのマスクには内側の円のマスクは含まれていません。これらのマスクは重複しないため、それぞれを独立して色付けし、結果を合計できます。

マスクと色を乗算して各マスクに1つの色を適用します。次に、結果を加えて複数の色を取得します。

vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;

これは、マスクが0.0から1.0の間の浮動小数点値にすぎないため機能します。色にマスクを乗算すると、その強度がスケーリングされます。

  • マスクが0.0の領域は色を消します。
  • マスクが1.0の領域は色を完全に表示します。
  • 中間の値は、色間のスムーズな遷移を生成します。

2つのカラーユニフォームを追加し、マスクされた色をfinal_colorに結合し、それを出力色として使用するようにシェーダーを更新します。ポータルの各部分は2つの要素から構成されます。

  • 色:カスタマイズできるようにユニフォームとして公開します。
  • マスク:その色がどこに表示されるかを決定します。
uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;

uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center = distance(UV, center);
  
  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;

  COLOR = vec4(portal_color, mask_rim);
  COLOR = vec4(final_color, mask_rim);
}

何かがまだ正しくありません...ポータルが中空です。

Radius
0.4

Rim Width
0.05

Softness
0.01

Color InnerRGB

Color RimRGB

最後のシェーダー命令をよく見てください。

vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;

COLOR = vec4(final_color, mask_rim);

最終的な色はポータルの見た目を制御しますが、どの部分が表示され、どの部分が破棄されるかを決定するのはアルファチャンネルです。

現在、アルファはリムマスクに設定されていますが、これはポータルの内側の領域をカバーしていません。ポータルと中心の両方を明らかにするために、リムと中心の両方をカバーするマスクが必要です。私たちが使用したマスクの1つはすでにそのプロパティを持っています。外側のマスクです。

float mask_outer = smoothstep(
  radius,
  radius - softness,
  distance_from_center
);

リングとは対照的に、外側のマスクの中心は塗りつぶされています。最終的な色のアルファチャンネルを外側のマスクに置き換えて、ポータルのリムと中心の両方を明らかにします。

void fragment() {
  // ...

  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;

  COLOR = vec4(final_color, mask_rim);
  COLOR = vec4(final_color, mask_outer);
}

Radius
0.4

Rim Width
0.05

Softness
0.01

Color InnerRGB

Color RimRGB

この時点で、ポータルはもはや単一色の形状ではありません。複数のレイヤーを組み合わせて構築されています。各レイヤーが最終的な画像の異なる部分を担当しています。

  • マスクは各部分がどこに表示されるかを決定します。
  • アルファチャンネルはポータルのシルエットを定義します。
  • 色は各部分がどのように見えるかを決定します。

シェーダーの素晴らしい点の1つは、いくつかの入力値を変更するだけで完全に異なるビジュアルを生成できることです。

このポータルを描画することもできましたが、形状、色、またはスタイルを変更したいと思った瞬間、新しいバリアントを作成する必要がありました。

シェーダーの入力を変更することで、完全に新しいビジュアルを生成できます。

細い紫色のポータル

放射性ポータル

OldDew
GDQuestの教師

これまでのところ、シェーダーは静的であり、結果はまだ少し期待外れです。次に、シェーダーを動かす方法を学びます。

14. 周期的関数でポータルをアニメーションさせる

ポータルは単なる退屈な幾何学的形状ではありません。それは時空の裂け目です。世界の構造そのものの亀裂です。未知への扉です。

これがどのように見えるべきかは、想像力にかかっています。ポータルとその境界が、それを開いたままにするために周囲の世界と絶えず戦っていると想像できるかもしれません。動きと不規則性を導入して、その感覚を作成しましょう。

美的感覚をシェーダーで制御する

エフェクトを設計する際には、それが存在する世界とそのルールを考慮することが良い考えです。

歪んだポータルは宇宙ゲームには適していますが、水中世界には渦巻き状のポータルがより適しているでしょう。

Portalシリーズのポータルも素晴らしい例です。そのゲームでは、あなたは白い、無菌の研究所で被験者を操作します。ポータルはキャラクターの高さに合わせて垂直であり、その外縁はほぼ完全な楕円形を形成します。

[画像: Both portals from the Portal Series]

Portalシリーズの両方のポータル

特定のエフェクトに適切なルックは、ゲームの世界とアートディレクションによってゲームごとに異なります。オンラインで見つかったシェーダーをアセットとして使用することもできますが、シェーダープログラミングを理解することで、最終的なルックに対する制御がさらに得られます。

動きを導入する前に、ポータルを完全に円形にしているものが実際には何であるかを考えてみましょう。

シェーダーは、スプライトの中心と各ピクセルとの距離を測定します。その距離を使用して、ピクセルが円の内側にあるか外側にあるかを決定します。

形状を変更して不規則にするにはどうすればよいでしょうか?その測定された距離を変更します。そうする前に、この距離に0.1を加算または減算した場合に何が起こるかを見てみましょう。効果は直感に反する可能性があるためです。

距離に加算および減算する

まず距離に加算してから、distance_from_center変数を変更してみてください。何に気づきますか?

float distance_from_center = distance(UV, center) + 0.1;

次に、0.1を引いてみてください。何に気づきますか?

float distance_from_center = distance(UV, center) - 0.1;

中心からの距離に0.1を減算する

距離を変更しないままにする

中心からの距離に0.1を加算する

これが結果です。

  • 距離に0.1を加算すると、ポータルは小さくなります。
  • 0.1を減算すると、ポータルは大きくなります。

0.1を加算すると、各ピクセルは実際よりも中心から遠いかのように動作します。その結果、ピクセルはより早く半径のしきい値に達し、ポータルが小さく見えます。0.1を減算すると逆の効果があります。

これをより詳細に説明できますか?

距離に0.1を加算または減算する前に、ピクセルが円の内側にあると見なされたのは、距離が半径より小さい場合でした。

distance < radius

距離に0.1を加算すると、比較は次のようになります。

distance + 0.1 < radius

これは方程式のように機能し、0.1項を方程式の右辺に移動させるために両辺から0.1を引くことができます。これは前の等価な方程式です。

distance < radius - 0.1

これは、距離に加算することが半径を減らすことと同じ効果があることを示しています!

ポータルを呼吸させる

この観察されたプロパティを使用して、ポータルを呼吸させることができます。ポータルを成長させたり縮小させたりします。これにより、シェーダーのアニメーション方法だけでなく、アニメーションをシームレスにループさせる方法も導入されます。

Godotシェーダーでは、値を時間とともにアニメートするためにTIME組み込みを使用します。TIMEは絶えず増加する数値です。エンジンの開始からの時間を提供します。

ポータルをアニメートするには、無限に増加しない値が必要です。代わりに、正の値と負の値の間で振動する数値が必要です。周期的関数(sin()やcos()など)を使用してこれを実現できます。入力値が増加すると、出力は-1.0と1.0の間で無限に振動します。

このグラフは、入力値が増加するにつれてsin()関数の出力を示しています。

[画像: Function sin in Desmos]

y = sin(x) の Desmos グラフ

このグラフはどうやって作成しましたか?

Desmos Graphing Calculatorを使用してこのグラフを作成しました。これは、関数と方程式をグラフとして視覚化できる無料のオンラインツールです。

左側の列に方程式、関数、変数を入力すると、右側のグラフに描画されます。プラットフォームの入門ガイドで詳細を学ぶことができます。

TIMEにsin(TIME)を追加して距離計算を更新します。出力を-0.1から0.1の範囲にするために、sin(TIME)に0.1を乗算します。ゼロと1の間の数値で乗算すると、結果がスケーリングされます。これにより、呼吸するポータルが得られます。

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;

uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

uniform float breathing_strength: hint_range(0.0, 0.1, 0.001) = 0.02;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center =
    distance(UV, center)
    + sin(TIME) * breathing_strength;

  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 final_color =
    mask_rim * color_rim +
    mask_inner * color_inner;

  COLOR = vec4(final_color, mask_outer);
}

sin(TIME)が振動するのはなぜですか?

TIMEはフレームごとに異なるため、任意の数学関数にプラグインしてアニメーションを作成できます。

サイン関数(sin())は数値を入力として受け取り、-1.0から1.0の間の値を返します。入力値が増加するにつれて、出力は-1.0と1.0の間で波のようなパターンで前後に移動するため、これを周期的関数と呼びます。

このグラフは、時間値が増加するにつれてsin(TIME)がどのように振動するかを示しています。

時間経過に伴うサイン関数のDesmosアニメーション

振動関数

多くのビジュアルエフェクトは、時間とともに継続的に振動する値に依存しています。この種の動きを生成する多くの関数がありますが、最も一般的に使用されるのはsin()とcos()(それぞれサイン関数とコサイン関数)です。それらは-1.0と1.0の間で振動するスムーズで繰り返される波を生成します。

これらの関数が波を生成する理由を理解するには、円との関係を理解する必要があります。このグラフでは、1単位の半径を持つ円の周回を一定速度で移動する点があります。

[画像: Sine and Cosine deriving from circular motion in Desmos]

円運動から導出されるサインとコサイン

サインは円周上の点の垂直位置です。コサインは円周上の点の水平位置です。

振動関数を制御する方法

振動の速度、強度、またはタイミングは、次のものを調整することで変更できます。

  • 周波数: 波が変化する速さ。関数の入力に掛けることで変更します。

sin ( x ⋅ frequency )

  • 振幅: 波の強さ、または高さ。関数全体に掛けることで変更します。

sin ( x ) ⋅ amplitude

  • オフセット: サイクルの位置における波のシフト。関数の入力に値を追加することで変更します。

sin ( x + offset )

それらを組み合わせて、汎用的でカスタマイズ可能な振動関数を取得します。

sin ( x ⋅ frequency + offset ) ⋅ amplitude

sin(TIME)に0.1を乗算したとき、振動の振幅を減らすことでエフェクトの強さを減らしました。ポータルの呼吸速度を変更するには、TIMEをTIME * 10.0に置き換えてサイン入力に代入します。

float distance_from_center =
  distance(UV, center) + sin(TIME * 10.0) * 0.1;

ポータルの呼吸をより繊細にするために、0.0から0.1の範囲のユニフォームを定義しましょう。効果をより繊細にするために、デフォルト値を0.02に設定できます。コードを次のように更新してください。

Breathing Strength
0.02

shader_type canvas_item;

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;

uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

uniform float breathing_strength: hint_range(0.0, 0.1, 0.001) = 0.02;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float distance_from_center =
    distance(UV, center) + sin(TIME) * breathing_strength;

  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 final_color =
    mask_rim * color_rim +
    mask_inner * color_inner;

  COLOR = vec4(final_color, mask_outer);
}

シェーダーをアニメートし、周期的関数を使用してアニメーションをループさせる方法を学びましたが、形状はまだ完全に円形です。次に、それを変形させる方法を学びましょう。

形状の変形

現在、ピクセル間の唯一の違いは中心からの距離です:distance(UV, center)。しかし、形状を縮小または拡大するとき、すべてのピクセルは同じルールに従い、形状を均一にスケーリングします。

sin(TIME) * breathing_strength

ポータルをより混沌とした見た目にするには、何らかの入力値に応じてピクセルの動作をわずかに変更する必要があります。

すでに知っているプロパティで実験してみましょう:スプライト内のピクセルの相対位置:UVです。sin()関数の出力を変更するために、UV.x座標を使用できます。

sin(TIME + UV.x) * breathing_strength

これにより、UV.x = 0.1を持つピクセルはsin(TIME + 0.1) * breathing_strengthを返し、UV.x = 0.7を持つ別のピクセルはsin(TIME + 0.7) * breathing_strengthを返すようになります。これにより、形状は経過時間だけでなく、スプライトの右側にあるピクセルに応じて変化します。

この例では、sin()関数を呼び出すときにTIME値にUV.xを追加しましたが、効果はほとんど見えません。

UV.xでオフセットされたサイン入力

これは、UV座標が0.0から1.0の範囲であるためです。その間隔では、サイン関数は完全な振動を完了しません。1サイクル(約6.28ラジアン)を完了するには2πラジアンかかります。

[画像: A complete sine oscillation compared to the UV values]

UV値と比較した完全なサイン振動

関数の周波数を増やすには、入力に1.0より大きい値を掛ける必要があります。例えば、UV.xに10を掛けると、円の幅全体でほぼ1.5回の振動が得られ、形状が曲がります。

このグラフは、UV.xに10を掛けると、1.5回の振動に近くなることを示しています。

[画像: Sin(x * 10) compared to the UV values]

Sin(x * 10) と UV 値の比較

sin()関数を呼び出すときにUV.xに10.0を掛けると、次のようになります。

sin(TIME + UV.x * 10.0) * breathing_strength

サインオフセットとUV.xを10.0でスケーリング

ここで、最新の実験を完全なシェーダーに配置して、簡単に確認しましょう。fragment()関数に振動を持つ新しい変数を作成し、それを使用するようにdistance_from_center計算を更新します。

Radius
0.4

Rim Width
0.05

Softness
0.01

Color InnerRGB

Color RimRGB

Breathing Strength
0.02

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;

uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

uniform float breathing_strength: hint_range(0.0, 0.1) = 0.02;

void fragment() {
  vec2 center = vec2(0.5, 0.5);

  float oscillation =
    sin(TIME + UV.x * 10.0) * breathing_strength;
  
  float distance_from_center =
    distance(UV, center) + oscillation;

  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;
  
  vec3 final_color =
    mask_rim * color_rim +
    mask_inner * color_inner;

  COLOR = vec4(final_color, mask_outer);
}

UV.xを具体的に10.0で乗算するのはなぜですか?

この数値に特別な意味はありません。任意です。数値10.0は、アニメーションが興味深く見えるのに十分なリップルを生成し、効果をはっきりと見ることができます。

しかし、周期的関数で使用できるいくつかの特別な数値があります。PI組み込みを使用して数値πを使用できます。これは約3.14であり、サインが半サイクルを完了する値です。

2.0を掛けると、完全なサイクルが得られます。これは非常によく使用されるため、GodotはそれをTAU(τ)組み込みとして提供しています。数学とシェーダーの両方で、τ = 2 ⋅ π です。

シェーダーで完全なサイクルを取得するには、sin(UV.x * TAU)と記述します。この計算の出力の視覚化を次に示します。

void fragment() {
  float cycle = sin(UV.x * TAU);
  COLOR = vec4(vec3(cycle), 1.0);
}

左側の明るい帯はサイン波の上側を示しており、画像の右側では値が負になります。追加の計算なしでは色として視覚化できません。シェーダーはそれを純粋な黒として描画しますが、値は-1.0から1.0の間で変化します。

TAUに任意の整数を掛けると、その回数だけ繰り返されるサイクルが得られます。このコードは2つのサイクルを生成します。

void fragment() {
  float cycle = sin(UV.x * TAU * 2.0);
  COLOR = vec4(vec3(cycle), 1.0);
}

2つの振動

そして、これは10サイクルの見た目です。

void fragment() {
  float cycle = sin(UV.x * TAU * 10.0);
  COLOR = vec4(vec3(cycle), 1.0);
}

10回の振動

シェーダーキャンバスに関数をグラフとしてプロットする方法

上記の質問と回答を確認した場合、一部の値は直接色として視覚化できないことがわかります。値がスプライト全体にグラフとして表示される視覚化テクニックがあります。

そのような場合、動きの関数をエフェクトの上に描画すると便利です。シェーダーキャンバスは2D画像なので、UV座標を使用して関数をプロットできます。

このシェーダーでは、振動を駆動する関数は次のとおりです。

y = sin ( t + x ⋅ 10.0 )

それをキャンバスにプロットするには、UV.yがsin(TIME + UV.x * 10.0)と等しいかどうかをチェックするif文を追加します。それが真の場合、ポータル色の代わりに緑色のピクセルを描画します。

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float oscillation = sin(TIME + UV.x * 10.0) * breathing_strength;
  float distance_from_center = distance(UV, center) + oscillation;
  float mask_outer = smoothstep(radius, radius - softness, distance_from_center);
  float mask_inner = smoothstep(radius - rim_width, radius - rim_width - softness, distance_from_center);
  float mask_rim = mask_outer - mask_inner;
  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;
  COLOR = vec4(final_color, mask_outer);

  if (UV.y == sin(TIME + UV.x * 10.0)) {
    COLOR = vec4(0.0, 1.0, 0.0, 1.0);
  }
}

現在、シェーダーは元のシェーダーと同じように見えます。画像には有限個のピクセルが含まれているため、サイン関数がそれらのいずれかのUV.y座標と正確に一致する値を返す可能性は非常に低いです。事実上ピクセルは条件を通過しません。

等価条件を置き換えて、十分に近いピクセルが線の部分と見なされるようにします。0.04のしきい値を使用すると、関数に見える太さが得られます。

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float oscillation = sin(TIME + UV.x * 10.0) * breathing_strength;
  float distance_from_center = distance(UV, center) + oscillation;
  float mask_outer = smoothstep(radius, radius - softness, distance_from_center);
  float mask_inner = smoothstep(radius - rim_width, radius - rim_width - softness, distance_from_center);
  float mask_rim = mask_outer - mask_inner;
  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;
  COLOR = vec4(final_color, mask_outer);

  if (UV.y == sin(TIME + UV.x * 10.0)) {
  if (
    UV.y + 0.04 > sin(TIME + UV.x * 10.0) &&
    UV.y - 0.04 < sin(TIME + UV.x * 10.0)
  ) {
    COLOR = vec4(0.0, 1.0, 0.0, 1.0);
  }
}

効果が関数形状によって変形されることに注意してください。サイン波が下がると、ポータルは押しつぶされます。波が上がると、ポータルは成長します。

これで、関数の変更が何をするかを直感的に伝えることができます。例えば、周波数を20に増やすと、ポータルがより頻繁に上下するはずです。

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  float oscillation =
    sin(TIME + UV.x * 20.0) * breathing_strength;
  float distance_from_center = distance(UV, center) + oscillation;
  float mask_outer = smoothstep(radius, radius - softness, distance_from_center);
  float mask_inner = smoothstep(radius - rim_width, radius - rim_width - softness, distance_from_center);
  float mask_rim = mask_outer - mask_inner;
  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;
  COLOR = vec4(final_color, mask_outer);

  if (
    UV.y + 0.04 > sin(TIME + UV.x * 20.0) &&
    UV.y - 0.04 < sin(TIME + UV.x * 20.0)
  ) {
    COLOR = vec4(0.0, 1.0, 0.0, 1.0);
  }
}

ついに、ポータルは円ではなくなったと言えます。呼吸強度が増すほど、左右に大きく揺れます。

パターンは現在かなり予測可能ですが、振動計算を再形成したり、より多くの関数を追加したりすることで、常に複雑さを増すことができます。

例えば、両方のUVコンポーネントを1つの波に含めるか、2つの波を別々に計算して合計することができます。

1つの対角波

2つの重ね合わされた波

最初の例では、両方の座標を同じサインで使用して、距離に追加するとエフェクトが対角線上に移動する波を得ています。

sin(TIME + UV.x * 10.0 + UV.y * 16.0)

一方、2番目の例では、垂直および水平コンポーネントを完全に別々の波として扱い、それらを重ね合わせます。

sin(TIME + UV.x * 10.0) + sin(TIME + UV.y * 16.0)

2番目の例では、パターンの識別が難しくなっていることに注意してください。

関数の組み合わせの詳細

加算は、関数を組み合わせる多くの方法の1つにすぎません。

複数の関数を組み合わせて新しいエフェクトを作成する方法についてさらに学びたい場合は、このトピックに関する詳細な学習ガイドを用意しました:シェーダー関数を組み合わせて興味深いVFXを作成する

このエフェクトをサイン波でさらに長く構築し続けることができます。

  • それらの速度、強度、方向、位相を変更できます。
  • より多くの波を重ね合わせることができます。
  • 中心周りの角度など、ピクセルの他のプロパティを使用できます。

各変更により、実験する別の種類の動きが得られます。

さまざまなスケールで多くの波を重ね合わせる

中心周りの角度を使用する

このコードのみでエフェクトを作成するテクニックは、プロシージャル生成(または短縮して「procgen」)としても知られています。特に、認識可能なリズムや方向性を持つ動きが必要な場合に役立ちます。

ただし、より混沌としたエフェクトを作成したい場合は、このアプローチはすぐにかなりのコードと数学を必要とする可能性があります。これは2つの方法であなたを制限する可能性があります。

  • コードの管理が難しくなったり、小規模なインディーチームで構築するのに時間がかかりすぎたりする可能性があります。
  • レンダリングパフォーマンスのコストが積み重なる可能性があります。グラフィックカードは非常に強力ですが、シェーダーがレンダリングする必要のあるすべてのピクセルに対してすべての操作を実行する必要があります。

プロシージャル生成に手を伸ばす前に、カオスを追加するための、はるかに簡単で高速な方法があります。そのためには、テクスチャをサンプリングできます。

OldDew
GDQuestの教師

15. 画像テクスチャのサンプリングと適用

これまでは、シェーダーが行うすべてを数学的表現で説明してきました。しかし、複雑な形状や描画された画像は、スプライトに適用するテクスチャで捉える方がはるかに簡単です。Godotはシェーダー内でテクスチャを使用できます。

シェーダーはテクスチャサンプリングと呼ばれるプロセスを通じてテクスチャを読み取ります。GDShaderでは、texture()組み込み関数を使用してこれを行います。2つのパラメータを取ります。

  1. サンプリングするテクスチャ
  2. サンプリングする座標

この例は、Sprite2Dノードのテクスチャをサンプリングして適用するためにGodotが内部で実行するコードを示しています。

void fragment() {
  COLOR = texture(TEXTURE, UV);
}

ここではいくつかのことが起こっているので、1つずつ見ていきましょう。

シェーダーは、サンプリングするテクスチャとしてTEXTURE、サンプリングする座標としてUVを使用してtexture()関数を呼び出します。

TEXTUREは、ノードのベーステクスチャを参照する組み込み変数です。この例では、Sprite2DノードのTextureプロパティに割り当てたGodotアイコンです。

[画像: The Godot icon used as the Sprite2D texture]

Sprite2Dテクスチャとして使用されるGodotアイコン

UV座標を2番目のパラメータとして渡すと、GPUがレンダリングする必要のある各ピクセルに対して、シェーダーはその位置に対応するテクセル(「テクスチャピクセル」の略)をサンプリングします。texture()関数はその色をvec4として返します。これはCOLOR組み込みに直接割り当てることができます。

例えば、UV座標(0.2, 0.3)は、テクスチャの幅の20%、高さの30%の位置にあるテクセル色をサンプリングします。このプロセスは、スプライトの各ピクセルに対して繰り返されます。

[画像: Pixel at the UV coordinates (0.2, 0.3) sampled from a lava texture]

UV座標がテクスチャ座標に正確に対応しない場合はどうなりますか?

テクスチャには固定数のテクセルがあり、それぞれサイズは同じです。例えば、テクスチャが32x32ピクセルであると言う場合、それは32ピクセルごとの32行があることを意味します。

32x32ピクセルのテクスチャで、UV座標(0.1, 0.1)での色をサンプリングしようとするとどうなりますか?

UV座標はパーセンテージを表すため、このコンテキストでの0.1は32の10%を意味します。それは3.2です!しかし、テクセルは固定数です。座標(2, 2)に1つ、(3, 3)に別のテクセルがありますが、(3.2, 3.2)にはテクセルがありません。

これを処理するために、Godotはその座標に適合する色を近似する必要があります。この決定は、テクスチャのフィルタモードに基づいています。最も一般的な2つはニアレストとリニアです(描画アプリケーションで見たことがあるかもしれません)。

ニアレストは、他の隣接するテクセルを無視して、UV座標に最も近いテクセルを返します。この図では、黄色のクロスが現在のUV座標を表し、破線が最も近いテクセルを表します。

[画像: Nearest sampling returning the closest texel]

ニアレストフィルタでは、関数は最も近いテクセルを返します。

リニアは、UV座標を囲むテクセルの色の混合を返します。

[画像: Linear sampling returning a mix of the neighboring texels]

リニアフィルタでは、関数は隣接するテクセルの加重平均を返します。

インスペクターの2Dノードでテクスチャのフィルタモードを変更できます。TextureFilterプロパティを探してください。

[画像: The texture's filter mode property]

テクスチャのフィルタモードプロパティ

以前使用したサイン波とはどのように異なるか、しばらく考えてみてください。

すべてのピクセルは同じ数式で数学的に記述されていました。

sin(TIME + UV.x * 20.0);

数式を変更すると、エフェクト全体がそれに従って変更されます。

テクスチャは異なる方法で機能します。値を計算する代わりに、シェーダーはすでにテクスチャに格納されている値をルックアップします。これにより、より多くの自由が得られます。

  • 値は完全に任意にすることができます。数学的パターンに従う必要はありません。
  • 画像編集プログラムで画像を編集して、テクスチャ全体のエフェクトを変更したり、テクスチャの一部のみを編集したりできます。

テクスチャがそれほど有用なら、なぜ常にそれらを使用しないのですか?

テクスチャには制限があります。

  • 解像度が有限です。 テクスチャは固定数のピクセルしか格納できないため、テクスチャが小さすぎると、テクスチャを使用したエフェクトは忠実度を失う可能性があります。
  • メモリを消費します。 テクスチャはグラフィックカードのメモリに格納する必要があります。テクスチャが大きいほど、より多くのメモリを使用します。
  • 柔軟性が低いです。 プロシージャルエフェクトの場合、ゲーム実行中でもユニフォームを使用して最終エフェクトを変更できます。テクスチャの場合、画像編集プログラムで手動で編集する必要があることがよくあります。

実際には、シェーダーでテクスチャとプロシージャル生成技術を頻繁に組み合わせて使用します。それぞれに長所と短所があります。両方を組み合わせて、最良の結果を得てください!

16. ノイズテクスチャでポータルを変形させる

自然現象にはある程度のランダム性があります。野原に散らばる草の葉、雲の形、あるいは目の色でさえ、常に不規則性があります。

ポータルの動きを自然に見せるには、不規則なテクスチャが必要です。

これまでのところ、TEXTUREはSprite2Dに割り当てられたGodotアイコンを参照していました。テクスチャがない場合、Sprite2Dノードはサイズを持たないため、エフェクトを描画する領域としてアイコンを使用しました。しかし、他の目的でアイコンを使用していないため、クラウドのような見た目のプロシージャルに生成されたテクスチャに置き換えてみましょう。ノイズテクスチャです。

ノイズテクスチャの作成

Sprite2Dノードを選択した状態で、インスペクターでTextureプロパティの横にあるリセットアイコンをクリックして、Godotアイコンテクスチャを削除します。

次に、Textureプロパティの横にある空のスロットをクリックし、NoiseTexture2Dを選択して、新しい空のノイズテクスチャに置き換えます。

[画像: Changing the texture to NoiseTexture2D]

テクスチャをNoiseTexture2Dに変更

新しく作成されたNoiseTexture2Dをクリックしてプロパティを展開します。Noiseプロパティの横にある空のスロットをクリックし、FastNoiseLiteを選択します。これにより、空の画像にカオスなテクスチャを生成できるアルゴリズムが作成されます。これはノイズテクスチャと呼ばれます。

[画像: Creating FastNoiseLite]

FastNoiseLiteの作成

FastNoiseLiteをクリックしてプロパティとノイズテクスチャのプレビューを表示します。次のプロパティを設定します。

  • Noise: Simplex Smoothに設定されていることを確認します。これはGodotが提供するプロシージャル生成アルゴリズムの1つです。
  • Frequency: 0.005に下げます。これはエフェクトの密度を制御します。

[画像: SimplexNoise Properties]

SimplexNoiseプロパティ

各プロパティが最終的なノイズテクスチャにどのように影響するかを把握するために、自由に試してみてください!

OldDew
GDQuestの教師

現在、このノイズテクスチャをコードで使用していませんが、フラグメント関数を次のコードに置き換えると、Godotアイコンテクスチャではなくノイズテクスチャがサンプリングされることになります。

void fragment() {
  COLOR = texture(TEXTURE, UV);
}

しかし、画像を表示することに限定されません。テクスチャをサンプリングすると、色の赤、緑、青のチャンネルを表す数値のシーケンスである色値が返されます。これらの値を使用して、ピクセルの元の中心からの距離をどれだけずらすか、ポータルの形状を変形させるかを計算できます。

ノイズテクスチャを使用してポータルを変形させる

ノイズテクスチャはグレースケールです。灰色の色合いは、赤、緑、青の値が等しい場合に現れるため、任意のチャンネルを使用して0.0から1.0の間の値を取得できます。

ノイズサンプルをオシレーションの代わりに、その値を中心からのピクセルの元の距離に加算して、シェーダーコードを次のように更新してノイズテクスチャからサンプリングします。

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

uniform float breathing_strength: hint_range(0.0, 0.1, 0.001) = 0.02;


void fragment() {
  vec2 center = vec2(0.5, 0.5);

  float noise_displacement = texture(TEXTURE, UV).r;

  float distance_from_center =
    distance(UV, center) + noise_displacement;

  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;

  COLOR = vec4(final_color, mask_outer);
}

これを次のように変更します。

shader_type canvas_item;

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

uniform float breathing_strength: hint_range(0.0, 0.1, 0.001) = 0.02;
uniform float displacement_strength: hint_range(0.0, 0.2) = 0.05;


void fragment() {
  vec2 center = vec2(0.5, 0.5);

  float noise_displacement = texture(TEXTURE, UV).r;

  float distance_from_center =
    distance(UV, center) + noise_displacement * displacement_strength;

  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;

  COLOR = vec4(final_color, mask_outer);
}

ノイズ値は0.0から1.0の範囲であるため、変位が強すぎ、形状が大きく消えてしまいます。変位をスケーリングダウンするユニフォームを追加しましょう。

Displacement Strength
0.05

shader_type canvas_item;

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

uniform float breathing_strength: hint_range(0.0, 0.1, 0.001) = 0.02;
uniform float displacement_strength: hint_range(0.0, 0.2) = 0.05;


void fragment() {
  vec2 center = vec2(0.5, 0.5);

  float noise_displacement = texture(TEXTURE, UV).r;

  float distance_from_center =
    distance(UV, center) + noise_displacement * displacement_strength;

  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;

  COLOR = vec4(final_color, mask_outer);
}

それはずっと良いです! しかし、なぜノイズがギザギザのエッジを持つこの方法でポータルを変形させるのでしょうか?

それは、各ピクセルがノイズテクスチャから異なる灰色をサンプリングしているためです。シェーダーはその値をピクセルのポータルの中心からの測定距離に加算します。

  • より明るいノイズ値は、計算された距離に大きな数値を加算し、ピクセルがポータルのエッジに早く到達するようにします。これにより、中心に引き寄せられます。
  • より暗いノイズ値は、より小さな数値を加算するため、それらの領域は元の輪郭に近いままです。

[画像: Different noise values affecting the final shape in their own way]

異なるノイズ値が最終形状にそれぞれ影響を与える方法

これがフラグメントシェーダーで変形を作成する方法です。ピクセルをオフセットするために、距離またはUV組み込みに値を加算および減算します。

この変更により、ポータルのアニメーションを失いました。よりカオスな変形がありますが、完全に静的です。テクスチャを時間とともにパンニングすることで、動きを再導入しましょう。

OldDew
GDQuestの教師
UVのオフセットによるテクスチャのパンニング

テクスチャをサンプリングする座標を変更することで、テクスチャをパン、スケール、または回転させることができます。例えば、ノイズテクスチャをサンプリングするときにUV.xに0.5を加えると、テクスチャが左に移動します。

void fragment() {
  vec2 sample_uv = vec2(UV.x + 0.5, UV.y);

  COLOR = texture(TEXTURE, sample_uv);
}

これは最初は奇妙に見えるかもしれませんが、実際には0.0から1.0の範囲外をサンプリングするときに期待されることです。UV.xに0.5を加えると、サンプル座標がシフトされるため、以前はテクスチャの右半分を読み取っていた点が、そのエッジを超えてしまいます。

例えば、UV座標vec2(0.0, 0.0)のピクセルは、現在vec2(0.5, 0.0)でテクスチャをサンプリングします。vec2(1.0, 0.0)のピクセルはvec2(1.5, 0.0)でサンプリングします。

なぜ追加するとテクスチャが左に移動するのですか?

UV座標に加算すると、テクスチャ自体ではなく、サンプリングポイントが移動します。

0.5を加算した後、UV.x == 0.0のピクセルは0.5からテクスチャを読み取ります。これは、元々中央にあったテクスチャコンテンツが左端に表示されるようになったことを意味します。

このテキストの説明だけを読むと直感的ではないかもしれません。シェーダーをよりよく理解するために、コードがピクセルに対して何をするかからズームアウトし、シェーダー全体としてどのように変化するかを観察する必要がある場合があります。

完璧な例えではありませんが、UV座標をシェーダーのキャンバスまたはカメラと考えることができます。画像編集プログラム内で、キャンバスまたはカメラを右に移動すると、ビューが右にシフトします。キャンバスに対して、画像は左にシフトします。

この短いクリップは、UV座標を右にシフトしたときに何が起こるかを示しています。移動する四角い領域は、UVがサンプリングしているものを示しています。

[画像: Moving the Canvas of the noise image]

ノイズ画像のキャンバスを移動する

しかし、1.5はテクスチャの通常の0.0から1.0のUV座標範囲外です。では、それをサンプリングしようとするとどうなりますか?デフォルトでは、テクスチャの最後のピクセルが引き伸ばされ、これらの奇妙な線が生成されます。しかし、テクスチャの繰り返しモードを通じてそれを変更できます。

繰り返しモード

テクスチャの繰り返しモードは、0.0から1.0の範囲外をサンプリングするときに何が起こるかを定義します。Godotは3つの繰り返しモードをサポートしています。

  • 無効: これはデフォルトモードであり、設定されている場合、テクスチャは繰り返されません。0.0から1.0の範囲外をサンプリングすると、座標は0.0から1.0の範囲にクランプされます。前の例では、vec2(1.5, 0.0)をサンプリングすると、実際にはvec2(1.0, 0.0)でテクスチャがサンプリングされます。1.0から1.5までのすべての水平座標で同じことが起こるため、それらはすべてテクスチャの右端からサンプリングされます。これが直線的な水平線を作成する理由です。
  • 有効: テクスチャが繰り返されます。テクスチャがすべての方向に無限にタイル状になっているかのように、サンプリングポイントが0.0から1.0の範囲でラップアラウンドします。例えば、UV座標がvec2(1.5, 0.0)の場合、テクスチャはvec2(0.5, 0.0)でサンプリングされます。
  • ミラー: テクスチャは繰り返されますが、他のタイルは反転されます。

インスペクターの2Dノードでテクスチャの繰り返しモードを変更できます。TextureRepeatを探してください。

[画像: The texture's repeat mode property]

テクスチャの繰り返しモードプロパティ

ノイズテクスチャが異なる繰り返しモードでサンプリングされた場合の前のシェーダーは次のとおりです。

繰り返しモード:無効

繰り返しモード:有効

繰り返しモード:ミラー

ポータルが連続的にアニメーションするようにしたいので、TextureRepeatプロパティを有効に設定します。これにより、TIMEを使用してテクスチャサンプリング位置をアニメートし、テクスチャをパンできるようになります。この例は、TIMEを使用してテクスチャをパンする方法を示しています。

void fragment() {
  vec2 sample_uv = vec2(UV.x + TIME, UV.y);

  COLOR = texture(TEXTURE, sample_uv);
}

繰り返される自然なパターンの錯覚は、テクスチャが繰り返されるときに現れるハードシームによって破られます。シームレスなノイズテクスチャを作成することでこれを修正できます。

インスペクターで、TextureSeamlessプロパティをオンにします。これにより、テクスチャのエッジをブレンドすることで、ノイズテクスチャがシームレスに繰り返されます。

場合によっては、Seamlessプロパティをオンにしてもシームがまだ見えることがあります。Seamless Blend Skirtプロパティでブレンド距離を調整することで、これを修正できます。

[画像: Video clip showing turning on the seamless property and increasing the seamless blend skirt property]

ノイズテクスチャをシームレスにする

パターンが認識しにくくなります。

void fragment() {
  vec2 sample_uv = vec2(UV.x + TIME, UV.y);

  COLOR = texture(TEXTURE, sample_uv);
}

ノイズに多様性を持たせるために、サンプリング座標の水平および垂直の両方の成分を変更して、ノイズが対角線上に移動するようにできます。

void fragment() {
  vec2 sample_uv = vec2(UV.x + TIME, UV.y + TIME);

  COLOR = texture(TEXTURE, sample_uv);
}

またはさらに良いことに、ユニフォームベクトルでノイズの方向を制御できます。

Noise Directionx
1
y
1

uniform vec2 noise_direction = vec2(1.0, 1.0);

void fragment() {
  vec2 sample_uv = UV + TIME * noise_direction;

  COLOR = texture(TEXTURE, sample_uv);
}

この場合、なぜ直接UVに時間を追加できるのですか?

上記のコードリストでは、TIME組み込みをnoise_directionベクトルに乗算しています。単一の数値をvec2値に乗算すると、コンピューターはvec2の各コンポーネントに数値を乗算し、新しいvec2値を返します。

結果はvec2値でもあるため、乗算の結果をUV組み込みに直接追加できます。

そのため、次のように記述する代わりに。

vec2 sample_uv = vec2(
  UV.x + TIME * noise_direction.x,
  UV.y + TIME * noise_direction.y
);

次のように簡略化できます。

vec2 sample_uv = UV + TIME * noise_direction;

私は、行を短くするだけでなく、理解しやすくすると考えています。ノイズ方向の値をUV座標に追加しています。

テクスチャパンニング速度をスケーリングするために、方向と速度をスケーリングするユニフォームを追加できます。ノイズ方向ベクトルのxおよびyコンポーネントを変更することもできますが、単一の数値を使用して速度をスケーリングする方が簡単です。

Noise Directionx
1
y
1

Noise Speed
0.1

uniform vec2 noise_direction = vec2(1.0, 1.0);
uniform float noise_speed : hint_range(0.0, 1.0) = 0.1;

void fragment() {
  vec2 sample_uv = UV + TIME * noise_direction * noise_speed;

  COLOR = texture(TEXTURE, sample_uv);
}

ノイズテクスチャをポータルシェーダーに適用して、時間とともに形状がどのように変形するかを見てみましょう。

ポータルシェーダーコードを更新します。noise_directionとnoise_speedユニフォームを追加し、sample_uvを計算し、ノイズテクスチャをサンプリングするときにUVの代わりにそれを使用します。

Displacement Strength
0.05

Noise Directionx
1
y
1

Noise Speed
0.1

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

uniform float breathing_strength: hint_range(0.0, 0.1, 0.001) = 0.02;
uniform float displacement_strength: hint_range(0.0, 0.2) = 0.05;

uniform vec2 noise_direction = vec2(1.0, 1.0);
uniform float noise_speed: hint_range(0.0, 1.0) = 0.1;


void fragment() {
  vec2 center = vec2(0.5, 0.5);

  float noise_displacement = texture(TEXTURE, UV).r;
  vec2 sample_uv = UV + TIME * noise_direction * noise_speed;
  float noise_displacement = texture(TEXTURE, sample_uv).r;

  float distance_from_center =
    distance(UV, center) + noise_displacement * displacement_strength;

  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;

  COLOR = vec4(final_color, mask_outer);
}

ノイズの方向や速度を変更すると、画像がジャンプするのはなぜですか?

エンジン開始からの時間であるTIME値を使用してシェーダーをアニメートしています。この数値は常に増加しています。

アニメーションパラメータ(noise_speedなど)を固定している限り、時間が上昇しているという事実は連続的なアニメーションを提供します。
しかし、noise_speedのようなユニフォームを変更すると、突然同じ大きなTIME値が得られますが、それを別の数値で乗算すると、完全に異なる結果が得られます。

例えば、TIMEが10でnoise_speedが1.0の場合、TIME * noise_speedは10になります。しかし、noise_speedを0.2に変更すると、TIME * noise_speedは突然2になります。

乗算の結果にジャンプがあり、この変更がシェーダーの残りの部分に伝播するため、画像もジャンプします。

ノイズ変形を配置したら、呼吸オシレーションを距離計算に戻して、エフェクトをより説得力のあるものにします。このウェブサイトで読みやすくするために、distance_from_center計算を複数の行に分割しました。

Breathing Strength
0.03

Displacement Strength
0.05

Noise Directionx
1
y
1

Noise Speed
0.1

shader_type canvas_item;

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

uniform float breathing_strength: hint_range(0.0, 0.1, 0.001) = 0.02;
uniform float displacement_strength: hint_range(0.0, 0.2) = 0.05;

uniform vec2 noise_direction = vec2(1.0, 1.0);
uniform float noise_speed: hint_range(0.0, 1.0) = 0.1;


void fragment() {
  vec2 center = vec2(0.5, 0.5);
  
  vec2 sample_uv = UV + TIME * noise_direction * noise_speed;
  float noise_displacement = texture(TEXTURE, sample_uv).r;
  float oscillation = sin(TIME);

  float distance_from_center =
    distance(UV, center) +
    noise_displacement * displacement_strength +
    oscillation * breathing_strength;
  
  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;

  COLOR = vec4(final_color, mask_outer);
}

呼吸オシレーションと移動するノイズテクスチャのおかげで、ポータルは私たちが求めていた不規則で継続的に変化する動きを持つようになりました。

これまでに構築したものに感謝し、シェーダーの多くのユニフォームを試してみてください。

Radius
0.4

Rim Width
0.05

Softness
0.01

Color InnerRGB

Color RimRGB

Breathing Strength
0.03

Displacement Strength
0.05

Noise Directionx
1
y
1

Noise Speed
0.1

shader_type canvas_item;

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;

uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

uniform float breathing_strength: hint_range(0.0, 0.1) = 0.03;
uniform float displacement_strength : hint_range(0.0, 0.2) = 0.05;

uniform vec2 noise_direction = vec2(1.0, 1.0);
uniform float noise_speed: hint_range(0.0, 1.0) = 0.1;

void fragment() {
  vec2 center = vec2(0.5, 0.5);
  
  vec2 sample_uv = UV + TIME * noise_direction * noise_speed;
  float noise_displacement = texture(TEXTURE, sample_uv).r;
  
  float oscillation = sin(TIME);

  float distance_from_center =
    distance(UV, center) +
    noise_displacement * displacement_strength +
    oscillation * breathing_strength;
  
  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;
  
  vec3 final_color =
    mask_rim * color_rim +
    mask_inner * color_inner;

  COLOR = vec4(final_color, mask_outer);
}

まだエフェクトをさらに押し進める方法はたくさんありますが、エフェクトにはしっかりした基盤ができました。フラットな色から始まったものが、設定可能でアニメーションするポータルになりました。

学んだこと:

  • すべてのピクセルに対して並列に実行されるプログラムを記述する方法。
  • COLOR、UV、TIME、TEXTUREのような組み込みの使用方法。
  • 色、位置、方向をベクトルで表現する方法。
  • 中間値を視覚化することで、シェーダーコードを理解しデバッグする方法。
  • step()やsmoothstep()のような組み込み関数を使用してマスクを作成およびスムーズ化する方法。
  • マスクを組み合わせて複雑な形状を作成する方法。
  • ユニフォームとユニフォームヒントを使用して意味のあるコントロールを公開する方法。
  • 時間とsin()のような振動関数を使用して値をアニメートする方法。
  • 数学的入力を変更することでエフェクトを再形成する方法。
  • テクスチャをサンプリングし、それらをアニメートする方法。
  • テクスチャのサンプリング方法をより細かく制御するために、フィルタリングおよび繰り返しモードを使用する方法。
  • 予測可能なプロシージャルモーション(オシレーション)とノイズを組み合わせて、自然に見えるバリエーションを作成する方法。

そしておそらく最も重要なこととして、最も有用なシェーダー開発習慣の1つである反復的な作業を実践しました。

複雑な問題を小さな問題に分解し、1ステップずつ解決しました。視覚化によって各出力を調査し、不明な点があるたびに実験しました。

このプロセスは、個々の関数を暗記することよりも重要であり、独自のシェーダーに取り掛かる際に練習を続けるべきです。

OldDew
GDQuestの教師

これまではシェーダーを孤立して作業してきました。スクリプトからシェーダーのユニフォームを制御することで、ゲーム世界でそれを使用し始めましょう。キャラクターやオブジェクトが近づいたときにポータルを開く方法を学びましょう。

コード参照:res://portal.gdshader

これが現時点での完全なシェーダーコードです。

shader_type canvas_item;

uniform float radius: hint_range(0.0, 0.5) = 0.4;
uniform float rim_width: hint_range(0.0, 0.3) = 0.05;
uniform float softness: hint_range(0.0, 0.2) = 0.01;

uniform vec3 color_inner: source_color = vec3(0.02, 0.04, 0.51);
uniform vec3 color_rim: source_color = vec3(0.23, 0.35, 0.65);

uniform float breathing_strength: hint_range(0.0, 0.1, 0.001) = 0.02;
uniform float displacement_strength: hint_range(0.0, 0.2) = 0.05;

uniform vec2 noise_direction = vec2(1.0, 1.0);
uniform float noise_speed: hint_range(0.0, 1.0) = 0.1;


void fragment() {
  vec2 center = vec2(0.5, 0.5);
  
  vec2 sample_uv = UV + TIME * noise_direction * noise_speed;
  float noise_displacement = texture(TEXTURE, sample_uv).r;
  float oscillation = sin(TIME);

  float distance_from_center =
    distance(UV, center) +
    noise_displacement * displacement_strength +
    oscillation * breathing_strength;

  float mask_outer = smoothstep(
    radius,
    radius - softness,
    distance_from_center
  );

  float mask_inner = smoothstep(
    radius - rim_width,
    radius - rim_width - softness,
    distance_from_center
  );

  float mask_rim = mask_outer - mask_inner;

  vec3 final_color = mask_rim * color_rim + mask_inner * color_inner;

  COLOR = vec4(final_color, mask_outer);
}

複数のテクスチャをシェーダーで使用できますか?

はい、sampler2Dユニフォームを使用してシェーダーで複数のテクスチャを使用できます。実際、シェーダーで複数のテクスチャが必要になることはよくあります。そのため、これらのテクスチャをよく使用します。

A sampler2Dは、任意のテクスチャを選択できるシェーダーパラメータを公開します。

uniform sampler2D my_texture;

[画像: Sampler2D shader parameter using a noise texture]

ノイズテクスチャを使用したSampler2Dシェーダーパラメータ

texture()関数を使用して、TEXTURE組み込みと同様にサンプリングできます。

uniform sampler2D my_texture;

void fragment() {
  COLOR = texture(my_texture, UV);
}

これはノードのメインテクスチャではないため、サンプリングおよび繰り返しモードは制御外です。

代わりに、ヒントを通じてこれらのプロパティを指定します。例えば、このコードスニペットは、ニアレストフィルタモードと有効な繰り返しモードを持つテクスチャを公開します。

uniform sampler2D my_texture : filter_nearest, repeat_enable;

1つの制限は、ミラーモード用のヒントがないことです。この動作が必要な場合は、自分で実装する必要があります。

公式ドキュメントでこれらのヒントをすべて確認できます:GDShader Uniform hints。