Die Mathematik in Gamedev ist einfach. Kurven und Wellen für die Wirkung von Regen in Unity

Published on January 12, 2019

Die Mathematik in Gamedev ist einfach. Kurven und Wellen für die Wirkung von Regen in Unity

    Hallo allerseits! Mein Name ist Grisha und ich bin der Gründer von CGDevs. Wir reden weiter über Mathematik oder so. Vielleicht ist die Hauptanwendung der Mathematik in der Spieleentwicklung und der Computergrafik im Allgemeinen VFX. Lassen Sie uns also über einen solchen Effekt sprechen - Regen oder vielmehr über seinen Hauptteil, der Mathematik erfordert - Wellen auf der Oberfläche. Wir werden konsequent einen Shader für Wellen auf der Oberfläche schreiben und dessen Mathematik analysieren. Bei Interesse - willkommen unter Katze. Github-Projekt ist beigefügt.



    Manchmal kommt es im Leben vor, dass ein Programmierer Tamburin aufnehmen und Regen fördern muss. Im Allgemeinen ist das Thema der Regenmodellierung selbst sehr tiefgreifend. Es gibt viele mathematische Arbeiten zu verschiedenen Teilen dieses Prozesses, von fallenden Tropfen und den damit verbundenen Effekten bis zur Verteilung der Tropfen im Volumen. Wir analysieren nur einen Aspekt - den Shader, mit dem wir einen Effekt erzeugen können, der einer Welle aus einem gefallenen Tropfen ähnelt. Es ist Zeit, das Tamburin aufzunehmen!


    Wellenmathematik

    Beim Durchsuchen des Internets finden Sie so viele lustige mathematische Ausdrücke zum Erzeugen von Wellen. Oft bestehen sie aus einer Art "magischen" Zahlen und einer periodischen Funktion ohne Begründung. Im Allgemeinen ist die Mathematik dieses Effekts jedoch recht einfach.

    Wir brauchen nur die Gleichung einer ebenen Welle im eindimensionalen Fall. Warum wir später die Ebene und eindimensional analysieren werden.

    Die ebene Wellengleichung kann in unserem Fall wie

    folgt geschrieben werden: Aresult = A * cos (2 * PI * (x / Wellenlänge - t * Frequenz));
    Wo:
    Aresult - die Amplitude an dem Punkt x zum Zeitpunkt t
    A - maximale Amplitude der
    Wellenlänge - die Wellenlänge des
    Frequenz - Frequenzwelle
    PI- PI-Nummer = 3.14159 (Float)

    Shader


    Lass uns mit Shadern spielen. Die Koordinate "-Z" ist für die "Spitze" verantwortlich. In Unity ist dies im 2D-Fall praktischer. Falls gewünscht, wird es nicht schwierig sein, den Shader in Y umzuschreiben.

    Als erstes benötigen wir die Gleichung eines Kreises. Die Welle unseres Shaders ist symmetrisch um das Zentrum. Die Gleichung eines Kreises im 2D-Fall wird wie folgt beschrieben:

    r ^ 2 = x ^ 2 + y ^ 2

    Wir benötigen einen Radius, daher hat die Gleichung die Form:

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

    und dies gibt uns Symmetrie um den Punkt ( 0, 0) im Netz, wodurch alles auf den eindimensionalen Fall einer ebenen Welle reduziert wird.

    Jetzt schreiben wir einen Shader. Ich werde nicht jeden Schritt des Schreibens eines Shaders analysieren, da dies nicht der Zweck des Artikels ist, sondern auf dem Standard Surface Shader von Unity basiert, dessen Vorlage über Create-> Shader-> StandardSurfaceShader abgerufen werden kann.

    Zusätzlich werden die für die Wellengleichung erforderlichen Eigenschaften hinzugefügt : _Frequency , _WaveLength und _WaveHeight . Property _Timer (kann Zeit mit der GPU verwendet werden, aber in der Entwicklung und anschließenden belebenden leichter wird es manuell zu steuern.

    Schreiben Sie eine Funktion eine Höhe getHeight (jetzt Koordinate Z) die Gleichung des Kreises in der Wellengleichung ersetzt

    einen Shader mit unserer Wellengleichung und der Gleichung des Kreises geschrieben - wir bekommen solch ein Effekt.

    Shader-Code
    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"
    }
    



    Wellen dort. Aber ich möchte, dass die Animation mit einer Ebene beginnt und endet. Dies hilft uns bei der Sinusfunktion. Durch Multiplikation der Amplitude mit sin (_Timer * PI) erhalten wir ein glattes Auftreten und Verschwinden von Wellen. Da _Timer Werte von 0 bis 1 annimmt und der Sinus bei Null und in PI Null ist, ist genau dies erforderlich.


    Während überhaupt nicht wie ein Tropfen Tropfen. Das Problem ist, dass die Wellenenergie gleichmäßig verloren geht. Fügen Sie die _Radius-Eigenschaft hinzu, die für den Effektbereich verantwortlich ist. Und multiplizieren Sie mit der Clamp-Amplitude (_Radius - rad, 0, 1) und Sie erhalten einen Effekt, der eher der Wahrheit entspricht.


    Nun, der letzte Schritt. Die Tatsache, dass die Amplitude an jedem einzelnen Punkt zu einem Zeitpunkt von 0,5 ihr Maximum erreicht, ist nicht ganz richtig. Diese Funktion sollte ersetzt werden.



    Dann fühlte ich mich ein wenig faul zu zählen, und ich multiplizierte einfach den Sinus mit (1 - _Timer) und bekam eine solche Kurve.



    Im Allgemeinen ist es aus mathematischer Sicht jedoch auch möglich, die gewünschte Kurve basierend auf der Logik auszuwählen, zu welchem ​​Zeitpunkt Sie den Peak und die ungefähre Form wünschen, und dann eine Interpolation auf diesen Punkten zu erstellen.

    Das Ergebnis war so ein Shader und Effekt.

    Shader-Code
    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"
    }
    




    Mesh mesh is important

    Zurück zum Thema des vorherigen Artikels . Die Wellen werden von einem Vertex-Shader implementiert, sodass das Mesh des Mesh eine relativ große Rolle spielt. Da die Art der Bewegung bekannt ist, wird die Aufgabe vereinfacht, aber im Allgemeinen hängt die endgültige visuelle Darstellung von der Form des Gitters ab. Der Unterschied wird bei hoher Polygonalität unwesentlich, aber für die Produktivität ist es umso besser, je kleiner die Polygone sind. Unten sehen Sie Bilder, die den Unterschied zwischen Rastern und Grafiken veranschaulichen.

    Richtig:



    Falsch:



    Selbst wenn die Anzahl der Polygone doppelt so hoch ist, wird im zweiten Netz das falsche Bild angezeigt (beide Netze werden mit Hilfe von Triangle.Net nur für verschiedene Algorithmen generiert).

    Letzte visuelle


    Ein spezieller Teil wurde zu einer anderen Version des Shaders hinzugefügt, um Wellen nicht ausschließlich in der Mitte, sondern an mehreren Punkten zu erzeugen. Wie das umgesetzt wird und wie man solche Parameter weitergeben kann, kann ich in den folgenden Artikeln sagen, ob das Thema interessant ist.

    Hier ist der Shader selbst:

    Ripple Vertex mit Pole
    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"
    }
    


    Das gesamte Projekt und seine Funktionsweise finden Sie hier . Zwar mussten einige der Ressourcen aufgrund von Gewichtsbeschränkungen des Githabs (HDR-Skybox und Auto) entfernt werden.

    Vielen Dank für Ihre Aufmerksamkeit! Ich hoffe, der Artikel wird jemandem nützlich sein, und es wurde ein wenig klarer, warum Sie Trigonometrie, analytische Geometrie (alles, was mit Kurven zu tun hat) und andere mathematische Disziplinen benötigen.