[Unity Shader] 水域レンダリングの基本 - 頂点ウェーブ

1. 頂点波形について:

広い水域の水域変化を表現するには、水位の全体的な動き変化を行う必要がある場合が多い。つまり、波の起伏効果を実現するために、平面の頂点が移動されます。さて、高速フーリエ変換や波の統計理論などの波の構成については、ゲームへの応用は比較的完璧です。今日は主に基本的な波形の実装である正弦波を行います。

1.1. 基本的な正弦波

平面をドラッグして頂点シェーダーを変更し、フラグメントシェーダーで海面の色を直接返します。

v2f o;
float3 p;
p = v.vertex;
p.y = sin(p.x);
//注意这里肯定不能在视口变换完后再求正弦,原因不用多说了吧?
o.vertex = UnityObjectToClipPos(p);

基本的な波形を取得します。
ここに画像の説明を挿入

1.2. 振幅パラメータ

主にピーク制御のために、振幅パラメータ _Amplitude を増やします。

p.y = _Amplitude * sin(p.x);

ここに画像の説明を挿入

1.3. 波長パラメータ

波長パラメータ _Wavelength は主に正弦関数の周期に影響します。つまり、関数は周期的な変化を完了するまでに時間がかかり、パフォーマンスの意味は水の波の幅です。
サイン関数 sin(x) の周期はデフォルトで 2π であることに注意してください。これをより直観的に制御するには、最終制御ベクトルとして k パラメーターを追加する必要があります。k を 2π/_Wavelength で解き、_Wavelength が大きいほど、最終的なパフォーマンスでの水の波の幅が大きくなることがわかります。

float k = 2 * UNITY_PI / _Wavelength;
p.y = _Amplitude * sin(k * p.x);

ここに画像の説明を挿入
ここで鋭いジッターという明らかな問題に注意してください。これは、プレーン自体の頂点の数が明らかに詳細すぎるパフォーマンスをサポートするには十分ではないことを示しています。次のステップでは、比較のために頂点を増やして 2 つの平面をやり直します。

1.4. 波速パラメータ

_Wavespeed、これについては多くの説明は必要ありません

p.y = _Amplitude * sin(k * (p.x + _Wavespeed * _Time.y));

ここに画像の説明を挿入
ここの1枚目はunity付属の面、左右の面は自作の20 20面と30 30面です波長が短いと頂点不足でハードエッジが発生するのがわかります。
ここに画像の説明を挿入
ヒント: Blender モデルを
Unity のデフォルト構成にエクスポートすると、サイズの差は 5 倍になりますが、それは維持されます。

1.5. 法線ベクトルを求める

現在の問題は、波形があっても平面が単なる皮膚の層のように見えるため、フラグメント シェーダーでのライティングの計算には法線ベクトルが必要であることです。
ここで頂点を調整したので、平面の元の法線情報が利用できてはならず、それを自分で解決する必要があります。
まず第一に、彼の接線方向を解決する必要があります。これは導関数を見つけることです。
幸いなことに、現時点ではベクトルは x 方向と y 方向でのみ変化します。つまり、z 方向の導関数は 0 です。導関数 T = ( x', y' , 0 )。
現在の変化は x であるため、最終導関数は x に関して導出されます、T = ( 1, k * _Amplitude * cos( k * x), 0)。
副接線の方向については、z 方向に沿った単位ベクトル (0, 0, 1) であると確信しています。
最後に、外積により法線ベクトルを取得します。
ここに画像の説明を挿入

//in vertex shader
float3 tangent = normalize(float3(1, k * _Amplitude * cos(f), 0));
float3 normal = cross(float3(0, 0, 1.0), tangent);
//normal直接传递使用即可

//in vertex shader
float3 LightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0 * dot(i.normal, LightDir);

カスタムの水面カラーをアンビエントとして使用し、拡散して返すために追加します。
ここに画像の説明を挿入

1.6. グリッドの精度について

波長が比較的長い場合、水波自体の振幅が大きい、つまり頂点間の相対変位が比較的小さいため、平面メッシュ自体の違いによる性能への影響はほとんど変わらないように見えます。波長が短い場合、メッシュ間の精度の違いがパフォーマンスに大きな影響を与える可能性があります。
波長が 2 の場合、 10 10、20 20、30 30のグリッド性能の差が特に顕著になります。
ここに画像の説明を挿入
そこで、ここでは 100 100 の高精度平面グリッドを紹介し、20
20、30 30、100 100のグリッドを比較します
ここに画像の説明を挿入
後続のケースでは 100 × 100 の平面のみが使用されます

1.7. 影補正

影の 3 要素セットを直接適用して、正弦波に影を追加します。デフォルトの影の計算にいくつかの問題があることは明らかです。法線の計算は正しいものの、頂点の変更は明らかに影に影響を与えません。シャドウ マップ、結果としてシャドウが非常に平坦に見えます。(ここでの視覚効果は変更されています。照明パラメーターを再度調整しただけです)
主な理由は、デフォルトのシャドウマッピング方法が頂点オフセットのシェーダーをサポートしていないため、シャドウキャスターを自分で記述する必要があることです。
ここに画像の説明を挿入
サーフェス シェーダーについては、比較的単純な解決策があり、シャドウの計算で頂点の変更に頂点シェーダーを使用します。

#pragma surface surf Standard fullforwardshadows vertex:vert addshadow
//#pragma surface <surface function> <lighting model> <optional parameters>

ただし、unlit シェーダの場合、#pragma を使用して頂点シェーダを直接指定することはサポートされていないため、シャドウ計算パスを自分で記述する必要があります。
実際、Shadowcaster には 3 つのセットもあります:
フラグメント構造定義で定義された V2F_SHADOW_CASTER、
光ポート変換用の頂点シェーダーに配置された TRANSFER_SHADOW_CASTER_NORMALOFFSET()、
フラグメント シェーダーに配置された SHADOW_CASTER_FRAGMENT(i)。

Pass
{
    
    
    //原本的顶点偏移pass,记得添加阴影三件套
}

Pass
{
    
    
    Tags {
    
    "LightMode"="ShadowCaster"}

    CGPROGRAM

    #pragma vertex vert 
    #pragma fragment frag
    #pragma multi_compile_shadowcaster

    #include "UnityCG.cginc"

    float _Amplitude;
    float _Wavelength, _Wavespeed;
    //阴影映射不需要颜色纹理相关的参数

    struct v2f
    {
    
    
        V2F_SHADOW_CASTER;
    };

    v2f vert(appdata_base v)
    {
    
    
        v2f o;
        float3 p;
        float k = 2 * UNITY_PI / _Wavelength;
        p = v.vertex;
        float f = k * (p.x + _Wavespeed * _Time.y);
        p.y = _Amplitude * sin(f);
        
        v.vertex.xyz = p;
        //无需进行视口变换,只需要给原有的vertex赋值
        
        TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
        
        return o;
    }

    fixed4 frag(v2f i): SV_Target
    {
    
    
    	//若需要做深度剔除,仍需要进行单独计算处理
        SHADOW_CASTER_FRAGMENT(i);
    }

    ENDCG
}

ここに画像の説明を挿入
頂点オフセットの計算方法を変更した場合は、それに応じて Shadowcaster の頂点計算方法も調整する必要があります。簡略化のため、今後、シャドウキャスター パスに関する追加の指示は個別に提供しません。

ガースナー波:

正弦波の方が単純ですが、これは明らかに少し単純化しすぎです。したがって、これに基づいてさらに前進する必要があります。
同様に、私たちが作成した頂点シェーダーは、実際にモデルの表面上の頂点 (スキンの層) を処理します。正弦波では、各頂点は正弦関数に従い、y 軸方向にのみ上下に移動します。ガースナー波では、頂点は y 軸に沿って移動しながら、x 軸に沿って移動します。
ここに画像の説明を挿入
X 軸の方向では、頂点のオフセットが正から負、そして正に変化することがわかります。これは、一般的なコサイン関数の特性です。
したがって、新しい頂点方程式は、元の x 値に基づくオフセットとしてコサイン関数を追加することになります:
vertex = (x + A cos(k x), A sin(k x), 0)
導出後、つまり、
T = (1 - A k sin(k x), A k cos(k x), 0)

副正接は変更されず、外積を直接使用して法線を解くことができます。

p.y = _Amplitude * sin(f);
p.x += _Amplitude * cos(f);

float3 tangent = normalize(float3(1 - k * _Amplitude * sin(f), k * _Amplitude * cos(f), 0));
float3 normal = normalize(cross(float3(0, 0, 1.0), tangent));

ここに画像の説明を挿入

2.1. 波の重なりの調整

結果として得られる波は良く見えるかもしれませんが、常にそうとは限りません。たとえば、波長と振幅の両方を小さくすると、個々の波が折り重なって見えるという奇妙な結果が生じました。
ここに画像の説明を挿入
ここに画像の説明を挿入
なぜこのような現象が起こるのでしょうか?これは主に、x 値の計算方法の調整と、振幅と波長の 2 つのパラメータの複合効果が原因で、異なる x 計算値の結果が重複します。
ここに画像の説明を挿入
なぜそのような状況が起こるのでしょうか?主に 2 つのパラメーターの影響により、x2-x1 の値が A*(cos(k x1) - cos(k x2))
の値より大きいことは保証できません。k が大きい場合、三角関数は大幅に圧縮され、一定範囲内の変化の範囲が非常に大きくなり、A*( cos(k x1) - cos(k x2))の最大値は 2A になります。明らかに、A 値は制御できない要素ですが、この状況をより適切に制御するために、1/k を使用して元の A (振幅) パラメータを置き換えます。k が増加すると、三角関数の変動が大きくなり、それに応じて 1/k が減少し、( cos(k x1) - cos(k x2))の最大値が最大値 2/k に減ります。さらに、_Steepness パラメーターを追加し、_Steepness /k を使用して元の振幅パラメーターを置き換えて、ピークをより適切に制御します。

float _Amplitude = _Steepness / k;
float f = k * (p.x + _Wavespeed * _Time.y);
p.y = _Amplitude * sin(f);
p.x += _Amplitude * cos(f);

ここに画像の説明を挿入
新しいパラメーターを使用すると、ピークの重複現象が大幅に軽減され、_Steepness をさらに下げると、それに応じてピーク全体が平坦化されることがわかります。

2.2. 現実の波の速度

実際の物理学では、波の速度は実際には波長に関連しています。つまり、
ここに画像の説明を挿入
物理的な問題についてはあまり話さず、パラメーターを直接置き換えるだけなので、将来的には _Wavespeed は必要なくなります。

float _Amplitude = _Steepness / k;
//新增c,波长相关的波速参数,原有的_Wavespeed被替换
float c = sqrt(9.8 / k);
float f = k * (p.x + c * _Time.y);
p.y = _Amplitude * sin(f);
p.x += _Amplitude * cos(f);

ここに画像の説明を挿入

2.3. 自己定義の方向波

x 軸を方向とし、xz 平面内の任意の直線を使用した波動公式について話したら、準備は完了です。つまり、x と z は同時に y 方向の計算結果に作用し、x と z 方向も現在のパラメーター値に応じてオフセットされます。
**二次元方向 (x1, z1)** の場合:
最初に三角関数に代入される f 値は次のようになります。

f =  k *(dot((x1, z1),(vertex.x,vertex.z)+ c * _Time.y)

したがって、オフセット頂点 Vo では、x、y、z の値は次のようになります。
x = x + x1 * _Steepness / k * cos(f)
y = _Steepness / k * sin(f)
z = z + z1 * _急峻さ / k * cos(f)

頂点オフセットの 3D 表現が完了したので、法線ベクトルを解決します。自己定義の方向波が関与することにより、x 軸と z 軸がすべて法線の解に関与することがわかります。
一般的な解決策は 2 つあり、1 つは波の方向に沿って正接と副接線を解決するもので、もう 1 つはそれぞれ x 方向と z 方向に従って解決するものです。(図に示されている法線が厳密には正確ではないことをご容赦ください。)
ここに画像の説明を挿入
波の方向に沿った接線の解法は基本的に不可能であるか、恐ろしく複雑であることは明らかです。x 方向と z 方向に沿ってそれぞれの偏導関数を解く解は、比較的複雑度が低くなります。法線が 1 つだけ指定されている場合、その 2 つの垂直接線を交差乗算した結果は一意ではないためです。
x 方向に沿って dx 導関数を実行して正接を取得します。
x = 1 - x1 * x1 * _Steepness * sin(f)
y = x1 * _Steepness * cos(f)
z = - z1 * x1 * _Steepness * sin(f)

z 方向に沿って dz 微分を実行して複接線を取得します。
x = - x1 * z1 * _Steepness * sin(f)
y = z1 * _Steepness * cos(f)
z = 1 - z1 * z1 * _Steepness * sin(f)

ここに画像の説明を挿入

2.4. 複数波の重ね合わせ

実際の海のシーンでは、多くの場合、複数の波が重ね合わされており、各波は異なるパラメータを持つことができます。元の設定では、3 つのパラメータを管理するのが非常に不便です。
単純に 4 次元変数を使用して積分し、関数を使用してこの 4 次元変数を処理します。
ここに画像の説明を挿入)

Properties
    {
    
    
    	//其他属性
		_WavwA("wave A: directionX, directionZ, _Steepness, _Wavelength", Vector) = (1, 1, 0.8, 3.0)
	}
float3 gerstner(float4 wave, float3 p, inout float3 tangent, inout float3 bitangent)
{
    
    
	///另附
}
v2f vert (appdata v)
     {
    
    
         v2f o;
         float3 p = v.vertex;
         float3 tangent = float3(0.0, 0.0, 0.0);
         float3 bitangent = float3(0.0, 0.0, 0.0);
         
         p += gerstner(_WavwA, v.vertex, tangent, bitangent);

         float3 normal = normalize(cross(bitangent, tangent));
         
         //其他变换和属性赋值,阴影计算
         
         return o;
     }

主に元の計算をカスタム関数に移動します

float3 gerstner(float4 wave, float3 p, inout float3 tangent, inout float3 bitangent)
{
    
    
    float k = 2 * UNITY_PI / wave.w;
    float _Amplitude = wave.z / k;
    float c = sqrt(9.8 / k);
    float f = k * ( dot(wave.xy, p.xz) + c * _Time.y);

    tangent += normalize(float3(1 - wave.x * wave.x * k * _Amplitude * sin(f), 
                                        wave.x * k * _Amplitude * cos(f), 
                                        - wave.x * wave.y * k * _Amplitude * sin(f)));

    bitangent += normalize(float3( - wave.y * wave.x * k * _Amplitude * sin(f), 
                                        wave.y * k * _Amplitude * cos(f), 
                                        1 - wave.y * wave.y * k * _Amplitude * sin(f)));
    
    return float3( wave.x * _Amplitude * cos(f), _Amplitude * sin(f), wave.y * _Amplitude * cos(f));
}

ここに画像の説明を挿入
事前準備が完了したら、アトリビュートで 2 つの波を直接定義し、頂点シェーダーに追加して、複数の波の重ね合わせを完了します。
ここに画像の説明を挿入

v2f vert (appdata v)
{
    
    
    v2f o;
    float3 p = v.vertex;
    float3 tangent = float3(0.0, 0.0, 0.0);
    float3 bitangent = float3(0.0, 0.0, 0.0);
    
    p += gerstner(_WavwA, v.vertex, tangent, bitangent);
    p += gerstner(_WavwB, v.vertex, tangent, bitangent);

    float3 normal = normalize(cross(bitangent, tangent));
    
    //变换,阴影计算等
}

ここに画像の説明を挿入
なお、複数の波を重ね合わせると波が重なる問題が再発するため、各 wave_Steepness の累積和が 1 を超えないように注意する必要があります。

2.4.1. 3波の重ね合わせ

パラメータは次のとおりで、通常、3 つの波は同じ割合の波長を採用します。
ここに画像の説明を挿入
ここに画像の説明を挿入
比較用の単一波
ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/misaka12807/article/details/131263570