water-fluid-shaders について
Unity の水や流体シェーダーの作成、変更、最適化を支援するスキルです。モバイルや WebGL の制約を重視し、テンプレートや機能別コスト、ノード構成を案内します。
主な機能
- 01モバイル向け性能制約
- 02URP HLSL のテンプレート
- 03Gerstner 波のガイド
- 04Shader Graph のノード設定
- 05水表現のコスト段階
活用シーン
- 01モバイル向け水シェーダーの作成
- 02岸辺の泡と深度色の追加
- 03頂点による波の動きの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ adevra/ unity-shader-agent-skills --skill water-fluid-shadersインストール後、Claude Code で/water-fluid-shadersを入力して読み込まれたか確認します。
詳細
モバイル Unity 向け水・流体シェーダー
TRIGGER
ユーザーが Unity で水、海、川、湖、池、流体、溶岩、液体のシェーダーを作成、変更、最適化したい場合に、このスキルを使用します。特にモバイルや WebGL 向けの場合に適用します。トリガーには、「水シェーダー」、「海」、「波」、「泡」、「海岸線」、「屈折」、「コースティクス」、「Gerstner」、「流体」、「深度カラーリング」、「スタイライズド水」、「トゥーン水」や、水に関連するビジュアルエフェクトへの言及が含まれます。また、NVJOB、bmjoy、Alexander Ameye の水シェーダーリポジトリへの言及があった場合も適用します。
コア規則 — モバイル向け水の制約
-
モバイルではテッセレーションを使用しません。 モバイル GPU はハードウェアテッセレーションを効率的にサポートしていません。代わりに、既存メッシュ上で頂点変位を使用します。
-
水シェーダーのテクスチャサンプルは 4 回以下にします。 モバイル向け水シェーダーの標準的な予算は、法線マップ 1 枚(タイリング、スクロール)、深度テクスチャ 1 枚(海岸線/泡用)、シーンカラー 1 枚(屈折用)、泡/ディテールテクスチャ 1 枚です。予算を超える場合は、まずシーンカラーによる屈折を削除します。
-
深度テクスチャと不透明テクスチャを URP Asset の設定で有効にする必要があります。 深度ベースの海岸線検出と屈折に必要です。これらを有効にするとコピー用パスが追加されるため、モバイルでは約 1ms の予算を確保します。コストが高すぎる場合は、代わりに頂点カラーによる深度表現を使用します。
-
法線は単一レイヤーにします。 デスクトップ向けの水では、異なるスケールの法線マップサンプルを 2~3 枚ブレンドすることがよくあります。モバイルでは、アニメーション用の UV スクロールとともに、法線マップのサンプルを 1 回だけ使用します。
-
Gerstner 波は頂点シェーダーでのみ計算します。 波による変位をフラグメントごとに計算しないでください。モバイルでは波のオクターブ数を 2~3 に制限し、デスクトップでは 4 以上にします。
-
ワールド位置と UV 座標を除き、すべての水の計算に半精度を使用します。
モバイル向け水シェーダー — 最小テンプレート(URP HLSL)
このテンプレートは、製品レベルの水シェーダーを作成するための出発点です。
Shader "Mobile/Water_Simple"
{
Properties
{
_ShallowColor ("Shallow Color", Color) = (0.3, 0.8, 0.9, 0.6)
_DeepColor ("Deep Color", Color) = (0.1, 0.2, 0.5, 0.9)
_DepthMaxDistance ("Depth Max Distance", Float) = 3.0
_NormalMap ("Normal Map", 2D) = "bump" {}
_NormalScale ("Normal Scale", Float) = 0.5
_WaveSpeed ("Wave Speed", Float) = 0.05
_FoamColor ("Foam Color", Color) = (1, 1, 1, 1)
_FoamDistance ("Foam Distance", Float) = 0.4
_FoamCutoff ("Foam Cutoff", Float) = 0.5
}
SubShader
{
Tags {
"RenderType" = "Transparent"
"Queue" = "Transparent"
"RenderPipeline" = "UniversalPipeline"
}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
CBUFFER_START(UnityPerMaterial)
half4 _ShallowColor;
half4 _DeepColor;
half _DepthMaxDistance;
float4 _NormalMap_ST;
half _NormalScale;
half _WaveSpeed;
half4 _FoamColor;
half _FoamDistance;
half _FoamCutoff;
CBUFFER_END
TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap);
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float4 screenPos : TEXCOORD1;
float3 positionWS : TEXCOORD2;
};
Varyings vert(Attributes v)
{
Varyings o;
VertexPositionInputs posInputs = GetVertexPositionInputs(v.positionOS.xyz);
o.positionCS = posInputs.positionCS;
o.positionWS = posInputs.positionWS;
o.screenPos = ComputeScreenPos(o.positionCS);
o.uv = TRANSFORM_TEX(v.uv, _NormalMap);
return o;
}
half4 frag(Varyings i) : SV_Target
{
// --- Depth-based coloring ---
float2 screenUV = i.screenPos.xy / i.screenPos.w;
float rawDepth = SampleSceneDepth(screenUV);
float sceneEyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);
float surfaceEyeDepth = i.screenPos.w;
half depthDiff = saturate((sceneEyeDepth - surfaceEyeDepth) / _DepthMaxDistance);
half4 waterColor = lerp(_ShallowColor, _DeepColor, depthDiff);
// --- Animated normal map (single sample) ---
float2 scrollUV = i.uv + _Time.y * _WaveSpeed;
half3 normal = UnpackNormalScale(
SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, scrollUV),
_NormalScale
);
// --- Shoreline foam ---
half foamDepth = saturate((sceneEyeDepth - surfaceEyeDepth) / _FoamDistance);
half foam = step(foamDepth, _FoamCutoff);
waterColor = lerp(waterColor, _FoamColor, foam * _FoamColor.a);
// --- Simple fresnel ---
half3 viewDir = normalize(_WorldSpaceCameraPos - i.positionWS);
half fresnel = pow(1.0h - saturate(dot(half3(0, 1, 0), viewDir)), 3.0h);
waterColor.a = lerp(waterColor.a, 1.0h, fresnel);
return waterColor;
}
ENDHLSL
}
}
Fallback "Universal Render Pipeline/Unlit"
}
Gerstner 波の実装
Gerstner 波は、リアルなトロコイド波の動きを生み出します。頂点シェーダー内でのみ計算します。
// Add to vertex shader — call for each wave octave
float3 GerstnerWave(float3 worldPos, float2 direction, float steepness,
float wavelength, float speed)
{
float k = 2.0 * PI / wavelength;
float c = sqrt(9.8 / k); // Phase speed from dispersion relation
float2 d = normalize(direction);
float f = k * (dot(d, worldPos.xz) - c * speed * _Time.y);
float a = steepness / k; // Amplitude
return float3(
d.x * a * cos(f),
a * sin(f),
d.y * a * cos(f)
);
}
// In vertex shader:
Varyings vert(Attributes v)
{
float3 worldPos = TransformObjectToWorld(v.positionOS.xyz);
// 2-3 wave octaves for mobile (vary direction, wavelength, steepness)
float3 wave1 = GerstnerWave(worldPos, float2(1, 0.5), 0.25, 8.0, 1.0);
float3 wave2 = GerstnerWave(worldPos, float2(0.3, 1), 0.15, 4.0, 1.2);
worldPos += wave1 + wave2;
// Recalculate normals from wave tangent/binormal
// (simplified — use cross product of partial derivatives)
Varyings o;
o.positionCS = TransformWorldToHClip(worldPos);
// ... rest of vertex setup
return o;
}
水の機能コスト別ティア
モバイル向けの予算に収まる機能を選ぶ際に使用します。
ティア 1:超低コスト(ローエンドモバイル、約 0.5ms)
- 頂点カラーまたは UV ベースの深度カラーリング(深度テクスチャなし)
- スクロールする法線マップ 1 枚
- 泡なし、屈折なし
- 不透明レンダリング(アルファブレンドなし)
ティア 2:標準的なモバイル向け(約 1~2ms)
- 深度テクスチャによる海岸線カラーリング
- 深度しきい値による海岸線の泡
- スクロールする法線マップ 1 枚
- フレネルベースのアルファ
- Gerstner 波のオクターブ数 1~2
ティア 3:高品質なモバイル向け(約 2~4ms)
- 屈折用の深度テクスチャと不透明テクスチャ
- 2 枚の法線マップのブレンド
- Gerstner 波(3 オクターブ)
- アニメーションする泡テクスチャ
- メインライトによるスペキュラハイライト
- コースティクス(投影テクスチャまたはシェーダー生成)
ティア 4:デスクトップ専用
- 平面反射または SSR
- テッセレーション
- FFT ベースの海洋シミュレーション
- サブサーフェススキャタリング
- Gerstner 波のオクターブ数 4 以上
Shader Graph の水 — ノード構成ガイド
Shader Graph を使用する場合、モバイル向けの効率的なノード構成は次のとおりです。
-
深度フェード:Scene Depth ノード → Fragment Eye Depth を減算 → 最大距離で除算 → 飽和 → 浅瀬色と深水色の間を補間
-
法線アニメーション:UV ノード → 加算(Time × 速度ベクトル) → Sample Texture 2D(法線マップ、Normal に設定) → Normal Strength ノード
-
海岸線の泡:同じ深度計算をより短い距離で行う → Step ノード → 泡の色を乗算 → 水の色に加算
-
フレネル:Fresnel Effect ノード(べき乗値は約 3) → ベース値と 1.0 の間でアルファを補間
-
頂点変位(波):Position ノード(Object Space) → 正弦/余弦を使ったカスタムサブグラフ → Position 出力に加算
ノードの合計数:モバイル向けの水シェーダーは 60 ノード未満に抑えます。Sub Graph を使って整理してください。オーバーヘッドは増えませんが、グラフを読みやすく保てます。
参考リポジトリ — コピーして学習
参考にするのに最適なオープンソースの水シェーダーは次のとおりです。
| リポジトリ | モバイル対応? | 機能 | ライセンス |
|---|---|---|---|
| nvjob/nvjob-water-shader-simple-and-fast | はい | 法線マッピング、視差、テッセレーションなし | MIT |
| nvjob/nvjob-water-shaders-v2 | はい | V1 + 反射 + 岸辺の泡 | MIT |
| bmjoy/Stylized-Water-Shader-Unity-URP | はい | URP、深度カラー、泡、屈折 | — |
| N7RX/Simple-Water-Shader | はい | 軽量でモバイルを優先 | — |
| danielshervheim/unity-stylized-water | 一部対応 | マテリアルプリセットを備えたスタイライズド表現(約953★) | — |
| MatrixRex/Uber-Stylized-Water | 一部対応 | Unity 6、高いカスタマイズ性 | — |
| tuxalin/water-shader | いいえ | マルチプラットフォーム、ガースナー波、コースティクス | — |
参考資料
- Alexander Ameye:スタイライズド水シェーダー — https://ameye.dev/notes/stylized-water-shader/
- Roystan:トゥーン水 — https://roystan.net/articles/toon-water/
- Cyanilux:2D 水シェーダーの解説 — https://www.cyanilux.com/tutorials/2d-water-shader-breakdown/
- Harry Alisavakis:水シェーダー(CC0) — https://halisavakis.com/
- Catlike Coding:フローシリーズ — https://catlikecoding.com/unity/tutorials/
- 80.lv:自然のシェーダー(水+砂) — https://80.lv/articles/tutorial-how-to-make-nature-shaders-in-unity
- 80.lv:溶岩ランプ(流体シミュレーション) — https://80.lv/articles/tutorial-creating-a-lava-lamp-shader-with-unity
- Brabl:モバイル向け水シェーダー — https://brabl.com/unity-5-mobile-water-shader/
- Linden Reid:シンプルな水シェーダー — https://lindenreidblog.com/2017/12/15/simple-water-shader-in-unity/
- Gabriel Aguiar:2D の水/滝の VFX — https://www.gabrielaguiarprod.com