Matemáticas en Gamedev es simple. Curvas y ondas de unidad para efecto lluvia

Hola a todos! Mi nombre es Grisha y soy el fundador de CGDevs. Sigamos hablando de matemáticas o algo así. Quizás la aplicación principal de las matemáticas en el desarrollo de juegos y gráficos de computadora en general es VFX. Así que hablemos de uno de esos efectos: lluvia, o más bien de su parte principal, que requiere matemáticas, ondas en la superficie. Sucesivamente escriba un sombreador para las ondas en la superficie y analice sus matemáticas. Si está interesado, bienvenido a cat. Proyecto Github adjunto.



A veces llega un momento en la vida cuando un programador tiene que agarrar una pandereta y pedir lluvia. En general, el tema del modelado de lluvia en sí mismo es muy profundo. Hay muchos trabajos matemáticos sobre diferentes partes de este proceso, desde la caída de gotas y los efectos asociados con esto hasta la distribución de gotas en volumen. Analizaremos solo un aspecto: el sombreador, que nos permitirá crear un efecto similar a la onda de una gota caída. ¡Es hora de tomar una pandereta!


Ola matemática

Al buscar en Internet, encontrará muchas expresiones matemáticas divertidas para generar ondas. A menudo consisten en algún tipo de números "mágicos" y funciones periódicas sin justificación. Pero en general, la matemática de este efecto es bastante simple.

Solo necesitamos una ecuación de onda plana en el caso unidimensional. ¿Por qué analizaremos plano y unidimensional un poco más tarde?

La ecuación de onda plana en nuestro caso se puede escribir como:

Aresult = A * cos (2 * PI * (x / waveLength - t * frecuencia));
Donde:
Aresult - amplitud en el punto x, en el tiempo t
A es la amplitud máxima
longitud de onda - longitud de onda
frecuencia - frecuencia de onda
PI - número PI = 3.14159 (flotante)

Shader


Juguemos con los sombreadores. Para el "top" será responsable de la coordenada -Z. Esto es más conveniente en el caso 2D en Unity. Si lo desea, el sombreador no será difícil de reescribir en Y.

Lo primero que necesitamos es la ecuación de un círculo. La onda de nuestro sombreador será simétrica sobre el centro. La ecuación del círculo en el segundo caso se describe como:

r ^ 2 = x ^ 2 + y ^ 2

necesitamos un radio, entonces la ecuación toma la forma:

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

y esto nos dará simetría sobre el punto (0, 0) en la malla, lo que reducirá todo al caso unidimensional de una onda plana.

Ahora escribamos un sombreador. No analizaré cada paso de escribir un sombreador, ya que este no es el propósito del artículo, pero la base se toma del Sombreador de superficie estándar de Unity, cuya plantilla se puede obtener a través de Crear-> Sombreador-> Sombreador de superficie estándar.

Además, se agregan las propiedades necesarias para la ecuación de onda: _Frequency , _WaveLength y _WaveHeight . Propiedad _Timer (sería posible utilizar el tiempo con gpu, pero durante el desarrollo y la animación posterior es más conveniente controlarlo manualmente.

Escribimos la función getHeight para obtener la altura (ahora esta es la coordenada Z) sustituyendo la ecuación circular en la ecuación de onda

Al escribir un sombreador con nuestra ecuación de onda y la ecuación circular, obtenemos este efecto.

Código de sombreador
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" } 



Hay olas Pero quiero que la animación comience y termine con un avión. La función seno nos ayudará con esto. Multiplicando la amplitud por sin (_Timer * PI), obtenemos una apariencia suave y la desaparición de las ondas. Como _Timer toma valores de 0 a 1, y el seno en cero y en PI es cero, esto es exactamente lo que necesita.


Si bien no es como una gota cayendo. El problema es que la energía de las olas se pierde de manera uniforme. Agregue la propiedad _Radius, que será responsable del radio del efecto. Y multiplicamos la amplitud de la pinza (_Radius - rad, 0, 1) y obtenemos ya un efecto más parecido a la verdad.


Bueno, el paso final. El hecho de que la amplitud en cada punto individual alcance su máximo en un tiempo igual a 0.5 no es del todo cierto, es mejor reemplazar esta función.



Luego me sentí un poco flojo para contar, y simplemente multipliqué el seno por (1 - _Timer) y obtuve esa curva.



Pero en general, desde el punto de vista de las matemáticas, aquí también puede seleccionar la curva deseada en función de la lógica en qué momento desea un pico y una forma aproximada, y luego construir la interpolación en estos puntos.

El resultado es tal sombreador y efecto.

Código de sombreador
 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" } 




Malla de malla es importante

Volviendo un poco al tema del artículo anterior . El sombreador de vértices implementa las ondas, por lo que la malla de la malla juega un papel bastante importante. Dado que se conoce la naturaleza del movimiento, la tarea se simplifica, pero en general, la imagen final depende de la forma de la cuadrícula. La diferencia se vuelve insignificante con una alta poligonalidad, pero para el rendimiento, cuantos menos polígonos, mejor. A continuación se muestran imágenes que ilustran la diferencia entre cuadrículas y elementos visuales.

Correctamente:



Equivocado:



Incluso con el doble de polígonos, la segunda malla da una imagen incorrecta (ambas mallas se generaron usando Triangle.Net, solo usando diferentes algoritmos).

Visual final


En una versión diferente del sombreador, se ha agregado una parte especial para crear ondas no estrictamente en el centro, sino en varios puntos. Cómo se implementa esto y cómo puede pasar tales parámetros, puedo decir en los siguientes artículos, si el tema es interesante.

Aquí está el sombreador en sí:

Vértice ondulado con poste
 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" } 


El proyecto en su conjunto y cómo funciona se puede encontrar aquí . Es cierto que parte de los recursos tuvieron que eliminarse debido a las limitaciones de peso del github (hdr skybox y car).

Gracias por su atencion! Espero que el artículo sea útil para alguien, y se hizo un poco más claro por qué la trigonometría, la geometría analítica (todo lo relacionado con las curvas) y otras disciplinas matemáticas pueden ser necesarias.

Source: https://habr.com/ru/post/es435828/


All Articles