初めてのSpatialシェーダー

あなたは独自のカスタムSpatialシェーダーの作成を開始することにしました。たぶん、シェーダーを使って行われたクールなトリックをオンラインで見たか、または SpatialMaterial が自分のニーズを十分に満たしていないことに気付いたのでしょう。いずれにせよ、あなたはあなた自身のシェーダーを書くことに決めました、そして、今、あなたはどこから始めるべきかを理解する必要があります。

このチュートリアルでは、Spatialシェーダーの作成方法を説明し、CanvasItem チュートリアルよりも多くのトピックを扱います。

Spatialシェーダーには、CanvasItemシェーダーよりも多くの機能が組み込まれています。Spatialシェーダーで期待されるのは、Godotが一般的なユースケースの機能をすでに提供しており、シェーダーでユーザーが行う必要があるのは、適切なパラメーターを設定することだけです。これは、特にPBR(物理ベースのレンダリング)ワークフローに当てはまります。

これは2部構成のチュートリアルです。この最初のパートでは、頂点関数のハイトマップからの頂点変位を使用して、簡単な地形を作成する方法を説明します。第2部 では、このチュートリアルの概念を取り入れ、海水シェーダーを作成してフラグメントシェーダーでカスタムマテリアルを設定する方法を説明します。

注釈

このチュートリアルでは、タイプ(vec2floatsampler2D)、関数などのシェーダーの基本的な知識を前提としています。これらの概念に不安がある場合は、このチュートリアルを完了する前に、`The Book of Shaders <https://thebookofshaders.com>`_のやさしい紹介を読むことをお勧めします。

マテリアルを割り当てる場所

3Dでは、オブジェクトは メッシュ を使用して描画されます。メッシュは、「サーフェス」と呼ばれる単位でジオメトリ(オブジェクトの形状)とマテリアル(色とオブジェクトが光に反応する方法)を格納するリソースタイプです。メッシュには、複数のサーフェス、または1つのサーフェスを含めることができます。通常、別のプログラム(Blenderなど)からメッシュをインポートします。しかし、Godotには、メッシュをインポートせずにシーンに基本的なジオメトリを追加できる PrimitiveMeshes もいくつかあります。

メッシュの描画に使用できるノードタイプは複数あります。主なものは MeshInstance ですが、ParticlesMultiMeshes を使用することもできます)、またはその他です。

通常、マテリアルはメッシュ内の特定のサーフェスに関連付けられていますが、MeshInstanceなどの一部のノードでは、特定のサーフェスまたはすべてのサーフェスのマテリアルをオーバーライドできます。

サーフェスまたはメッシュ自体にマテリアルを設定すると、そのメッシュを共有するすべてのMeshInstanceはそのマテリアルを共有します。ただし、複数のメッシュインスタンスで同じメッシュを再利用したいが、インスタンスごとに異なるマテリアルを使用する場合は、Meshinstanceでマテリアルを設定する必要があります。

このチュートリアルでは、MeshInstanceのマテリアルをオーバーライドする機能を利用するのではなく、メッシュ自体にマテリアルを設定します。

セットアップ

新しい MeshInstance ノードをシーンに追加します。

[インスペクタ]タブの[Mesh]の横で[空]をクリックし、[新規 PlaneMesh]を選択します。次に、表示される平面の画像をクリックします。

これにより、シーンに PlaneMesh が追加されます。

次に、ビューポートで、「透視投影」というボタンの左上隅をクリックします。メニューが表示されます。メニューの中央には、シーンの表示方法のオプションがあります。 「ワイヤフレーム表示」を選択します。

これにより、平面を構成する三角形を見ることができます。

../../../_images/plane.png

次に、Subdivide WidthSubdivide Depth32 に設定します。

../../../_images/plane-sub-set.png

Mesh には、さらに多くの三角形があることがわかります。これにより、作業する頂点が増え、細部を追加できるようになります。

../../../_images/plane-sub.png

PrimitiveMeshe は、PlaneMeshのように、1つのサーフェスしか持たないため、マテリアルは配列の代わりに1つしかありません。[空]と表示されている[Material]の横をクリックし、[新規 ShaderMaterial]を選択します。次に、表示される球をクリックします。

[Shader]横の[空]と表示されている場所をクリックし、[新規 Shader]を選択します。

作成された"Shader"をクリックするとシェーダーエディタが表示され、最初のSpatialシェーダーの作成を開始する準備ができました!

シェーダーマジック

../../../_images/shader-error.png

すでにエラーが発生していることに注意してください。これは、シェーダーエディタがその場でシェーダーをリロードするためです。 Godotシェーダーで最初に必要なのは、どのタイプのシェーダーかを宣言することです。これは空間シェーダーなので、変数 shader_typespatial に設定します。

shader_type spatial;

次に vertex() 関数を定義します。vertex() 関数は Mesh の頂点が最終シーンのどこに現れるかを決定します。これを使用して各頂点の高さをオフセットし、平らな平面を小さな地形のように見せます。

頂点シェーダーを次のように定義します:

void vertex() {

}

vertex() 関数に何もない場合、Godotはデフォルトの頂点シェーダーを使用します。 1行追加するだけで簡単に変更を開始できます。

void vertex() {
  VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}

この行を追加すると、次のような画像が表示されます。

../../../_images/cos.png

さて、これを紐解きましょう。 VERTEXy 値は増加しています。そして、cossin への引数として、VERTEXxz コンポーネントを渡します。これにより、x 軸と z 軸に波のような外観が与えられます。

とりあえず、私たちが達成したいのは、小さな丘の様な外観です。cossin はすでに丘のように見えます。そのためには、cos および sin 関数の入力にスケーリングを施します。

void vertex() {
  VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
../../../_images/cos4.png

これは良い感じですが、それでもまだとがりすぎて反復的です。もう少し面白くしましょう。

ノイズハイトマップ

ノイズは、地形の外観を模倣するための非常に人気のあるツールです。これは、各丘の高さが異なることを除いて、丘が繰り返されるコサイン関数に似ていると考えてください。

Godotは、シェーダーからアクセスできるノイズテクスチャを生成するための NoiseTexture リソースを提供します。

シェーダーのテクスチャにアクセスするには、シェーダーの上部近くの vertex() 関数の外側に次のコードを追加します。

uniform sampler2D noise;

This will allow you to send a noise texture to the shader. Now look in the inspecter under your material. You should see a section called "Shader Params". If you open it up, you'll see a section called "noise".

その横にある[空]をクリックして、[新規 NoiseTexture]を選択します。次に、NoiseTextureで[Noise]と表示されている場所の横をクリックし、[新規 OpenSimplexNoise]を選択します。

OpenSimplexNoise は、高さマップを生成するためにNoiseTextureによって使用されます。

設定すると、次のようになります。

../../../_images/noise-set.png

Now, access the noise texture using the texture() function. texture() takes a texture as the first argument and a vec2 for the position on the texture as the second argument. We use the x and z channels of VERTEX to determine where on the texture to look up. Note that the PlaneMesh coordinates are within the [-1,1] range (for a size of 2), while the texture coordinates are within [0,1], so to normalize we divide by the size of the PlaneMesh 2.0 and add 0.5. texture() returns a vec4 of the r, g, b, a channels at the position. Since the noise texture is grayscale, all of the values are the same, so we can use any one of the channels as the height. In this case we'll use the r, or x channel.

float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
VERTEX.y += height;

注: xyzw はGLSLの rgba と同じなので、上記の texture().x の代わりに texture().r を使用できます。詳細については、 OpenGLのドキュメント を参照してください。

このコードを使用すると、テクスチャがランダムに見える丘を作成することがわかります。

../../../_images/noise.png

Right now it is too spiky, we want to soften the hills a bit. To do that, we will use a uniform. You already used a uniform above to pass in the noise texture, now let's learn how they work.

Uniform(ユニフォーム)

Uniform変数を使用すると、ゲームからシェーダーにデータを渡すことができます。シェーダーの効果を制御するのに非常に便利です。Uniform は、シェーダーで使用できるほぼすべてのデータ型です。Uniformを使用するには、キーワード uniform を使用して Shader で宣言します。

地形の高さを変えるユニフォームを作りましょう。

uniform float height_scale = 0.5;

Godotでは、Uniformを値で初期化できます。ここでは、height_scale0.5 に設定されています。シェーダーに対応するマテリアルで関数 set_shader_param() を呼び出すことで、GDScriptからUniformを設定できます。 GDScriptから渡される値は、シェーダーで初期化するために使用される値よりも優先されます。

# called from the MeshInstance
mesh.material.set_shader_param("height_scale", 0.5)

注釈

Changing uniforms in Spatial-based nodes is different from CanvasItem-based nodes. Here, we set the material inside the PlaneMesh resource. In other mesh resources you may need to first access the material by calling surface_get_material(). While in the MeshInstance you would access the material using get_surface_material() or material_override.

set_shader_param() に渡される文字列は Shader のUniform変数の名前と一致しなければならないことに注意してください。Shader 内のどこでもUniform変数を使用できます。ここでは、0.5 を任意に乗算する代わりに、これを使用して高さの値を設定します。

VERTEX.y += height * height_scale;

今ではずっと良く見えます。

../../../_images/noise-low.png

Uniformを使用すると、フレームごとに値を変更して、地形の高さをアニメーション化することもできます。Tween と組み合わせると、単純なアニメーションに特に役立ちます。

ライトとの相互作用

まず、ワイヤーフレームをオフにします。これを行うには、ビューポートの左上にある[透視投影]と表示された場所をもう一度クリックし、[通常表示]を選択します。

../../../_images/normal.png

メッシュの色が平坦になることに注意してください。これは、照明が平坦だからです。ライトを追加しましょう!

最初に、シーンに OmniLight を追加します。(訳注: 追加後に少し上に持ち上げると説明画像と同じようになります)。

../../../_images/light.png

ライトが地形に与える影響を見ることができますが、奇妙な見た目です。問題は、ライトが平面であるかのように地形に影響していることです。これは、ライトシェーダーが Mesh の法線を使用してライトを計算するためです。

法線はメッシュに保存されますが、シェーダーでメッシュの形状を変更しているため、法線は正しくありません。これを修正するには、シェーダーで法線を再計算するか、ノイズに対応する法線テクスチャを使用します。 Godotは両方を簡単におこなえます。

頂点関数で新しい法線を手動で計算し、NORMAL に設定することができます。NORMAL に設定すると、Godotは面倒な照明計算をすべて行います。この方法については、このチュートリアルの次の部分で説明しますが、今はテクスチャから法線を読み取ります。

その代わりに、再びNoiseTextureを利用して法線を計算します。これを行うために、2番目のノイズテクスチャで値を渡します。

uniform sampler2D normalmap;

別のOpenSimplexNoiseを使用して、この2番目のuniformテクスチャを別のNoiseTextureに設定します。ただし、今回は[As Normalmap]をチェックしてください。

../../../_images/normal-set.png

さて、これは頂点ごとの法線ではなく法線マップなので、fragment() 関数で割り当てます。fragment() 関数については、このチュートリアルの次のパートで詳しく説明します。

void fragment() {
}

特定の頂点に対応する法線がある場合、NORMAL を設定しますが、テクスチャからの法線マップがある場合は、NORMALMAP を使用して法線を設定します。この方法で、Godotはメッシュの周りのテクスチャのラッピングを自動的に処理します。

最後に、ノイズテクスチャと法線マップテクスチャの同じ場所から確実に読み込むために、vertex() 関数から fragment() 関数に ``VERTEX.xz``の位置を渡します。varyingを使ってそれを行います。

Above the vertex() define a vec2 called tex_position. And inside the vertex() function assign VERTEX.xz to tex_position.

varying vec2 tex_position;

void vertex() {
  ...
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  ...
}

そして、fragment() 関数から vertex_position にアクセスできるようになりました。

void fragment() {
  NORMALMAP = texture(normalmap, vertex_position).xyz;
}

法線を設定すると、ライトはメッシュの高さに動的に反応します。

../../../_images/normalmap.png

ライトをドラッグすることもでき、照明の状態が自動的に更新されます。

../../../_images/normalmap2.png

このチュートリアルの完全なコードは次のとおりです。 Godotが面倒な部分のほとんどを処理するので、それほど長くないことがわかります。

shader_type spatial;

uniform float height_scale = 0.5;
uniform sampler2D noise;
uniform sampler2D normalmap;

varying vec2 tex_position;

void vertex() {
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  VERTEX.y += height * height_scale;
}

void fragment() {
  NORMALMAP = texture(normalmap, tex_position).xyz;
}

これがこの部分のすべてです。 Godotの頂点シェーダーの基本を理解できたと思います。このチュートリアルの次の部分では、この頂点関数に付随するフラグメント関数を作成し、この地形を動く波の海に変えるためのより高度なテクニックをカバーします。