Gamedevの数学は簡単です。 Unityの雨効果の曲線と波紋

みなさんこんにちは! 私の名前はグリシャで、CGDevsの創設者です。 数学などについて話を続けましょう。 おそらく、ゲーム開発およびコンピューターグラフィックス全般での数学の主な用途はVFXです。 そこで、そのような効果の1つ-雨、または数学を必要とするその主要部分-表面の波紋について話しましょう。 表面の波紋のシェーダーを連続して記述し、その数学を分析します。 興味があれば、猫へようこそ。 Githubプロジェクトが添付されました。







プログラマーがタンバリンを手に取り、雨を要求しなければならない瞬間が来ることがあります。 全体的に、雨のモデリングのトピック自体は非常に深いです。 このプロセスのさまざまな部分には、液滴のドロップとこれに関連する効果から液滴の体積分布まで、さまざまな数学的な作業があります。 1つの側面のみを分析します-シェーダー。ドロップされたドロップからの波に似た効果を作成できます。 タンバリンに挑戦する時です!





数学波



インターネットを検索すると、波紋を生成するための多くの面白い数式が見つかります。 多くの場合、これらは何らかの種類の「マジック」番号と正当化のない周期関数で構成されています。 しかし、一般的に、この効果の数学は非常に単純です。



1次元の場合、平面波方程式のみが必要です。 後でフラットで1次元を分析する理由。



この場合の平面波方程式は、 次のように書くことができます。



Aresult = A * cos(2 * PI *(x / waveLength-t *周波数));

どこで:

結果 -時間tでのポイントxの振幅

Aは最大振幅です

波長 -波長

周波数 -波の周波数

PI - PI番号= 3.14159(フロート)



シェーダー





シェーダーで遊んでみましょう。 「トップ」は座標-Zを担当します。 これは、Unityの2Dの場合により便利です。 必要に応じて、シェーダーをYに書き換えることは難しくありません。



最初に必要なのは、円の方程式です。 シェーダーの波は中心に対して対称になります。 2dの場合の円の方程式は次のように記述されます。



r ^ 2 = x ^ 2 + y ^ 2



半径が必要なので、方程式は次の形式を取ります。



r = sqrt(x ^ 2 + y ^ 2)



これにより、メッシュ内のポイント(0、0)について対称性が得られ、すべてが平面波の1次元のケースになります。



次にシェーダーを作成します。 これは記事の目的ではないため、シェーダーを書くすべてのステップを分析するわけではありませんが、基本はUnityのStandard Surface Shaderに基づいています。テンプレートはCreate-> Shader-> StandardSurfaceShaderから取得できます。



さらに、波動方程式に必要なプロパティ_Frequency_WaveLengthおよび_WaveHeight追加されます。 プロパティ_Timer (gpuで時間を使用することは可能ですが、開発および後続のアニメーション中は、手動で制御する方が便利です。



getHeight関数を記述して、波の方程式に円の方程式を代入することにより、高さ(現在はZ座標)を取得します。



波の方程式と円の方程式でシェーダーを記述すると、この効果が得られます。



シェーダーコード
Shader "CGDevs/Rain/RainRipple" { Properties { _WaveHeight("Wave Height", float) = 1 _WaveLength("Wave Length", float) = 1 _Frequency("Frequency", float) = 1 _Timer("Timer", Range(0,1)) = 0 _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"= "Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness, _Metallic, _Frequency, _Timer, _WaveLength, _WaveHeight; fixed4 _Color; half getHeight(half x, half y) { const float PI = 3.14159; half rad = sqrt(x * x + y * y); half wavefunc = _WaveHeight * cos(2 * PI * (_Frequency * _Timer - rad / _WaveLength)); return wavefunc; } void vert (inout appdata_full v) { v.vertex.z -= getHeight(v.vertex.x, v.vertex.y); } void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = _Color.a; } ENDCG } FallBack "Diffuse" }
      
      









波があります。 しかし、アニメーションは飛行機で始まり、飛行機で終わるようにします。 サイン関数はこれに役立ちます。 振幅にsin(_Timer * PI)を掛けると、滑らかな波の出現と消滅が得られます。 _Timerは0から1までの値を取り、ゼロおよびPIのサインはゼロなので、これはまさに必要なものです。





落下するようなものではありませんが。 問題は、波のエネルギーが均等に失われることです。 _Radiusプロパティを追加します。これは、エフェクトの半径を担当します。 そして、クランプ振幅(_Radius-rad、0、1)を掛けると、すでに真実に近い効果が得られます。





さて、最後のステップ。 個々のポイントの振幅が0.5に等しい時間で最大に達するという事実は完全に真実ではないため、この関数を置き換えることをお勧めします。







それから、私は数えるのが少し遅すぎると感じ、サインに(1-_Timer)を掛けて、そのような曲線を得ました。







しかし、一般的には、数学の観点から、ここでピークと近似形状が必要な時点のロジックに基づいて目的の曲線を選択し、これらの点で補間を作成することもできます。



結果は、このようなシェーダーと効果です。



シェーダーコード
 Shader "CGDevs/Rain/RainRipple" { Properties { _WaveHeight("Wave Height", float) = 1 _WaveLength("Wave Length", float) = 1 _Frequency("Frequency", float) = 1 _Radius("Radius", float) = 1 _Timer("Timer", Range(0,1)) = 0 _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"= "Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness, _Metallic, _Frequency, _Timer, _WaveLength, _WaveHeight, _Radius; fixed4 _Color; half getHeight(half x, half y) { const float PI = 3.14159; half rad = sqrt(x * x + y * y); half wavefunc = _WaveHeight * sin(_Timer * PI) * (1 - _Timer) * clamp(_Radius - rad, 0, 1) * cos(2 * PI * (_Frequency * _Timer - rad / _WaveLength)); return wavefunc; } void vert (inout appdata_full v) { v.vertex.z -= getHeight(v.vertex.x, v.vertex.y); } void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = _Color.a; } ENDCG } FallBack "Diffuse" }
      
      











メッシュメッシュは重要です



前の記事のトピックに少し戻ります 。 波は頂点シェーダーによって実装されるため、メッシュのメッシュはかなり大きな役割を果たします。 モーションの性質は既知であるため、タスクは単純化されますが、一般的に、最終的なビジュアルはグリッドの形状に依存します。 高ポリゴンでは違いはわずかですが、パフォーマンスを向上させるには、ポリゴンが少ないほど良いでしょう。 以下は、グリッドとビジュアルの違いを示す写真です。



正しく:







間違った:







2倍のポリゴンがある場合でも、2番目のメッシュは間違った視覚効果をもたらします(両方のメッシュは、異なるアルゴリズムを使用してTriangle.Netを使用して生成されます)。



最終ビジュアル





シェーダーの別のバージョンに特別なパーツが追加され、中央ではなく複数のポイントに波が作成されます。 トピックが興味深い場合は、これがどのように実装され、そのようなパラメーターをどのように転送するかを次の記事で説明できます。



シェーダー自体は次のとおりです。



極を持つリップル頂点
 Shader "CGDevs/Rain/Ripple Vertex with Pole" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _Normal ("Bump Map", 2D) = "white" {} _Roughness ("Metallic", 2D) = "white" {} _Occlusion ("Occlusion", 2D) = "white" {} _PoleTexture("PoleTexture", 2D) = "white" {} _Color ("Color", Color) = (1,1,1,1) _Glossiness ("Smoothness", Range(0,1)) = 0 _WaveMaxHeight("Wave Max Height", float) = 1 _WaveMaxLength("Wave Length", float) = 1 _Frequency("Frequency", float) = 1 _Timer("Timer", Range(0,1)) = 0 } SubShader { Tags { "IgnoreProjector" = "True" "RenderType" = "Opaque"} LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.0 sampler2D _PoleTexture, _MainTex, _Normal, _Roughness, _Occlusion; half _Glossiness, _WaveMaxHeight, _Frequency, _Timer, _WaveMaxLength, _RefractionK; fixed4 _Color; struct Input { float2 uv_MainTex; }; half getHeight(half x, half y, half offetX, half offetY, half radius, half phase) { const float PI = 3.14159; half timer = _Timer + phase; half rad = sqrt((x - offetX) * (x - offetX) + (y - offetY) * (y - offetY)); half A = _WaveMaxHeight * sin(_Timer * PI) * (1 - _Timer) * (1 - timer) * radius; half wavefunc = cos(2 * PI * (_Frequency * timer - rad / _WaveMaxLength)); return A * wavefunc; } void vert (inout appdata_full v) { float4 poleParams = tex2Dlod (_PoleTexture, float4(v.texcoord.xy, 0, 0)); v.vertex.z += getHeight(v.vertex.x, v.vertex.y, (poleParams.r - 0.5) * 2, (poleParams.g - 0.5) * 2, poleParams.b , poleParams.a); } void surf (Input IN, inout SurfaceOutputStandard o) { o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb * _Color.rgb; o.Normal = UnpackNormal(tex2D(_Normal, IN.uv_MainTex)); o.Metallic = tex2D(_Roughness, IN.uv_MainTex).rgb; o.Occlusion = tex2D(_Occlusion, IN.uv_MainTex).rgb; o.Smoothness = _Glossiness; o.Alpha = _Color.a; } ENDCG } FallBack "Diffuse" }
      
      







プロジェクト全体とその仕組みについては、 こちらをご覧ください 。 確かに、github(hdr skyboxとcar)の重量の制限により、リソースの一部を削除する必要がありました。



ご清聴ありがとうございました! この記事が誰かに役立つことを願っており、三角法、解析幾何学(曲線に関連するすべて)、その他の数学の分野が必要になる理由が少し明確になりました。



All Articles