Creación de un orbe 2d con líquido en Unity

By Sergei Alekseev(Game Developer) on Junio 08 2020

Views

14292

#gamedev#unity#tutorial#shaders

La introducción

Déjame clarificar el resultado final aquí mismo. Éste es lo que vamos a hacer en este tutorial:

Orb with liquid. Final result

Recientemente en Grimwood Team me encargué de crear algún progreso de nivel. La idea principal es que el gamer debería mantener jugando mientras algunas cosas se llenan de líquido y luego explotan. La principal inspiración hacer algo así nos ha venido de Hollow Knight. ¿Recuerda?, hay una "barra" de progreso de salud tal como un cráneo se llena con el alma líquida(al menos es lo que veo).

Implementemos estas cosas.

PS. Se puede encontrar el código fuente en el repositorio de este tutorial.

Prerrequisitos

Unity 2019.3.2f1 - Lo estoy usando en el momento de escribir este tutorial. Yo uso la versión inglesa así que habrán los nombres de menús en inglés y también estaré usando los nombres ingleses para los objetos.

La textura de orbe con poca transparencia(efecto de vidrio).

La textura de máscara de orbe - en mi caso es un negro círculo de la talla del orbe.

La textura del borde del orbe - como efecto adicional para la composición general.

La textura del líquido - esa textura se usará para llenar el orbe(He creado algunos en mi repositorio para fines de prueba).

El tutorial

Cree un proyecto 2D en Unity con una escena vacía y llamada "LiquidOrb".

Mueva todos los sprites de requisitos previos a "LiquidOrb/Assets/Sprites".

Creemos una jerarquía para nuestro orbe

  • Haga clic derecho, seleccione "Create Empty". Nómbrelo "OrbObject"
  • Haga clic derecho en "OrbObject", seleccione "Create Empty". Nómbrelo "Water"
  • Haga clic derecho en "OrbObject", seleccione "Create Empty". Nómbrelo "OrbBorder"
  • Haga clic derecho en "OrbObject", seleccione "Create Empty". Nómbrelo "Orb"

Debería tener la siguente jerarquía:

Agreguemos Sprite Renderers a los objetos "Agua", "OrbBorder", "Orb" con los sprites correspondientes. Para hacerlo seleccione el objeto y en el Inspector seleccione "Add Component" y encuentre "Sprite Renderer".

Afine la coordinada z de los objectos así que el objeto "Water" estará atrás de los objetos "OrbBorder" y "Orb", el objeto "Orb" estará entre los "Water" y "OrbBorder". He configurado 1, 0, -1 valores para "Agua", "Orb", "OrbBorder" correspondientemente.

3D view of the effect

Configure la escala del objeto "Agua" para que cubra "Orbe".

Water sprite adjust

Estamos listos para hacer magia.

Seleccione "OrbObject", en el Inspector seleccione "Add Component", encuentre y agregue el componente "Sprite Mask". Adjunte orb_mask sprite al componente "Sprite Mask". Ahora debería ver los bordes de la máscara en la escena(línea naranja-marrón).

Orb with Mask Parent

No le preocupa, será un círculo perfecto cuando terminemos. Unity lo hace así.

Enmascaremos nuestro objeto "Agua". Seleccione "Water" y en el componente Sprite Renderer cambie la propiedad "Mask Interaction" a "Visible Inside Mask".

Sprite Renderer Mask Settings

Y ahora podemos ver algo que se vea como el resultado final.

Orb liquid masked

Ahora necesitamos crear un nuevo material con shader que proporcione el comportamiento líquido.

He encontrado un efecto súper genial en shadertoy:

Eso es casi que necesitamos.

He bifurcado este shader y hice un pequeño cambio para hacer lo "que necesitamos" más claramente:

Se ve bien, eh? Traigamos este efecto a nuestro proyecto de Unity.

Venga al proyecto "LiquidOrb". Agregue carpeta "Assets/Shaders". Haga clic derecho en la carpeta "Shaders", seleccione "Create", "Shader", "Image Effect Shader"(O cualquier shader - eliminaremos su contenido más tarde). Abra este shader, vamos a verificar lo que tenemos adentro.

Shader "Hidden/LiquidShader"
{
	//Muchas cosas están aquí
}

Cambiemos "Hidden/LiquidShader" a "MyShaders/LiquidShader"(para hacerlo disponible más tarde) y eliminar todo el contenido del shader.

Shader "MyShaders/LiquidShader"
{
}

No necesitamos contenido generado automáticamente, no funcionará como esperamos nuestro efecto porque no tiene reglas para mascar las cosas, etc. La idea aquí es tomar el integrado shader de sprite y modificarlo con el shader de Shadertoy. He cargado los shaders integrado a mi repositorio de github. También puede encontrar estos shaders en el sitio de Unity: seleccione la versión que te gusta, haga clic en "Downloads" y seleccione "Builtin Shaders".

Vamos a actualizar nuestro LiquidShader con el contenido del shader integrado de Unity. Usaré la versión de mi repositorio.

Ahora vamos a verificar que todo funciona bien:

  • Cree carpeta "Assets/Materials".
  • Haga clic derecho "Materials", seleccione "Create", "Material", nómbrelo "LiquidMaterial".
  • Seleccione "LiquidMaterial". En el Inspector seleccione "Shader", encuentre y seleccione "MyShaders", "LiquidShader".

Hemos creado un material para nuestro sprite de liquido. Seleccione objecto "Water" en la jerarquía. En el Componente "Sprite Renderer" del inspector seleccione Material, encuentre y especifique "LiquidMaterial".

Como resultado, todo debería estar bien y funcionar cómo estaba funcionando antes. Eso significa que nuestras cosas personalizadas funcionan. Ahora vamos a integrar el shader de Shadertoy.

El shader liquido de Shadertoy es un shader de fragmentos. Creemos una función separada para el shader de fragmentos. En "LiquidShader" venga a la sección "Pass".

Pass
{
CGPROGRAM
    #pragma vertex SpriteVert
    #pragma fragment SpriteFrag // We need to change this part
    #pragma target 2.0
    #pragma multi_compile_instancing
    #pragma multi_compile_local _ PIXELSNAP_ON
    #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
    #include "UnitySprites.cginc"
ENDCG
}

Ahora SpriteFrag es la función de shader de fragmentos que es implementado en #include "UnitySprites.cginc".

Entonces vamos a crear nuestra función personalizada llamada frag y la ponemos debajo de #include "UnitySprites.cginc":

Pass
{
CGPROGRAM
    #pragma vertex SpriteVert
    #pragma fragment frag // we've changed the name of the func to "frag". The implementation can be found below
    #pragma target 2.0
    #pragma multi_compile_instancing
    #pragma multi_compile_local _ PIXELSNAP_ON
    #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
    #include "UnitySprites.cginc"

    fixed4 frag (v2f i) : SV_Target
    {
        fixed4 col = fixed4(0.0,0.0,0.0,0.0);
        return col;
    }
ENDCG
}

Ahora en la escena deberíamos ver que esa agua no se muestra porque hemos cambiado el shader de fragmentos para devolver el color fixed4(0.0, 0.0, 0.0, 0.0) para cada posición que viene a la función frag.

Vamos a copiar el código del shader de Shadertoy y pegarlo a LiquidShader debajo de la función frag:

Pass
{
CGPROGRAM
    #pragma vertex SpriteVert
    #pragma fragment frag // Hemos cambiado el nombre de la función a "frag". Se puede encontrar la implementación abajo
    #pragma target 2.0
    #pragma multi_compile_instancing
    #pragma multi_compile_local _ PIXELSNAP_ON
    #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
    #include "UnitySprites.cginc"

    fixed4 frag (v2f i) : SV_Target
    {
        fixed4 col = fixed4(0.0,0.0,0.0,0.0);
        return col;
    }
		vec4 drawWater(vec4 water_color, sampler2D color, float transparency, float height, float angle, float wave_strength, float wave_ratio, vec2 uv);
		
		void mainImage(out vec4 fragColor, in vec2 fragCoord)
		{
		    vec2 uv = fragCoord / iResolution.xy;
		    
		    float WATER_HEIGHT = abs(sin(iTime * 0.5));
		    vec4 WATER_COLOR = vec4(0.5, 0.5, 0.53, 1.0);
		    float WAVE_STRENGTH = 3.0;
		    float WAVE_FREQUENCY = 8.0;
		    float WATER_TRANSPARENCY = 0.4;
		    float WATER_ANGLE = 2.0;
		    
		    fragColor = drawWater(WATER_COLOR, iChannel0, WATER_TRANSPARENCY, WATER_HEIGHT, WATER_ANGLE, WAVE_STRENGTH, WAVE_FREQUENCY, uv);
		}
		
		vec4 drawWater(vec4 water_color, sampler2D color, float transparency, float height, float angle, float wave_strength, float wave_frequency, vec2 uv)
		{
		    angle *= uv.y/height+angle/1.5; //3D effect
		    wave_strength /= 1000.0;
		    float wave = sin(10.0*uv.y+10.0*uv.x+wave_frequency*iTime)*wave_strength;
		    wave += sin(20.0*-uv.y+20.0*uv.x+wave_frequency*1.0*iTime)*wave_strength*0.5;
		    wave += sin(15.0*-uv.y+15.0*-uv.x+wave_frequency*0.6*iTime)*wave_strength*1.3;
		    wave += sin(3.0*-uv.y+3.0*-uv.x+wave_frequency*0.3*iTime)*wave_strength*10.0;
		    
		    if(uv.y - wave <= height)
		        return mix(
		        mix(
		            texture(color, vec2(uv.x, ((1.0 + angle)*(height + wave) - angle*uv.y + wave))),
		            water_color,
		            0.6-(0.3-(0.3*uv.y/height))),
		        texture(color, vec2(uv.x + wave, uv.y - wave)),
		        transparency-(transparency*uv.y/height));
		    else
		        return vec4(0,0,0,0);
		}
ENDCG
}

Como puede observar, hay algunas diferencias entre los idiomas. No le preocupe. Vamos a hacer los cambios siquientes:

  • Copie el título de la funcion frag y reemplace el título de la funcion mainImage. Elimine la funciona frag anterior.
  • Reemplace todos los vec4 a fixed4, vec2 a fixed2
  • Reeplace fixed2 uv = fragCoord / iResolution.xy; a fixed2 uv = i.texcoord;
  • Unity no tiene iTime integrado, pero sí tiene variable _Time. Entonces reemplace iTime a _Time o tengo una mejor sugerencia de crear una variable flotante llamada iTime y asígnele _Time en la parte superior de las funciones frag y drawWater. También multiplicaría el valor de iTime porque el tiempo de Shadertoy defiere del tiempo de Unity y puede parecer muy lentamente en Unity. Entonces, vamos a multiplicarlo por 10.0: iTime = _Time * 10.0
  • Reemplace iChannel0 a _MainTex
  • Unity no tiene la función texture pero tiene tex2D. Reemplace todas las funciones llamadas func a tex2D
  • Unity no tiene la función mix pero tiene lerp. Cambie todas mix a lerp
  • En el lugar donde fragColor donde se define agregue fixed4 fragColor = .... Y agregue la declaración return fragColor

Ahora puede ejecutar la escena y ver que el orbe se está llenando lentamente por el liquido. ¡Estamos casi allí!

Actualicemos el shader para hacer todos los variables WATER_HEIGHT, WATER_COLOR, WAVE_STRENGTH, WAVE_FREQUENCY, WATER_TRANSPARENCY, WATER_ANGLE ajustables del inspector.

Modifique la sección Propiedades de LiquidShader a lo siguiente:

Properties
{
    [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
    _Color ("Tint", Color) = (1,1,1,1)
    _Progress ("Progress", Range(0.0, 1.0)) = 0.5 // Nueva
    _WaterColor ("WaterColor", Color) = (1.0, 1.0, 0.2, 1.0) // Nueva
    _WaveStrength ("WaveStrength", Float) = 2.0 // Nueva
    _WaveFrequency ("WaveFrequency", Float) = 180.0 // Nueva
    _WaterTransparency ("WaterTransparency", Float) = 1.0 // Nueva
    _WaterAngle ("WaterAngle", Float) = 4.0 // Nueva
    [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
    [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
    [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
    [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {}
    [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
}

Agregue lo siguiente a la sección Pass debajo de #include "UnitySrpites.cginc":

#include "UnitySprites.cginc"

float _Progress; // Var Nueva
fixed4 _WaterColor; // Var Nueva
float _WaveStrength; // Var Nueva
float _WaveFrequency; // Var Nueva
float _WaterTransparency; // Var Nueva
float _WaterAngle; // Var Nueva

fixed4 drawWater(fixed4 water_color, sampler2D color, float transparency, float height, float angle, float wave_strength, float wave_frequency, fixed2 uv);

Actualice la función frag a:

fixed4 frag (v2f i) : SV_Target
{
    fixed2 uv = i.texcoord;
    float WATER_HEIGHT = _Progress; // Progres Personalizado
    float4 WATER_COLOR = _WaterColor; // Color Personalizado
    float WAVE_STRENGTH = _WaveStrength; // Fuerza Personalizada
    float WAVE_FREQUENCY = _WaveFrequency; // Frequencia Personalizada
    float WATER_TRANSPARENCY = _WaterTransparency; // Transparencia de liquido Personalizada
    float WATER_ANGLE = _WaterAngle; // Ángulo del liquido personalizado
    
    fixed4 fragColor = drawWater(WATER_COLOR, _MainTex, WATER_TRANSPARENCY, WATER_HEIGHT, WATER_ANGLE, WAVE_STRENGTH, WAVE_FREQUENCY, uv);
    return fragColor;
}

Si seleccione LiquidMaterial de la carpeta Materials en el inspector podrá ver los parámetros personalizados. Puede ejecutar la escena y cambiarlos durante la ejecución de la escena. Genial, eh?

Ahora puede tener el acceso a sus parámetros de sus scripts:

public class LiquidProgressController : MonoBehaviour
{
    public SpriteRenderer LiquidRenderer;
    public float AccumulatedTime;
    public float DelayCoef = 0.5f;

    // Update se llama cada frame
    void Update()
    {
        AccumulatedTime += Time.deltaTime * DelayCoef;
        LiquidRenderer.material.SetFloat("_Progress", Mathf.Abs(Mathf.Sin(AccumulatedTime)));
    }
}

La conclusión

Eso fue el básico de la migración shaders de Shadertoy a Unity. Me gustaría mencionar algunas cosas.

Si quiere ejecutarlo en la plataforma móvil entonces se necesita desmarcar "Disable Depth and Stencil" en "Player Settings"(para iOS y Android). De lo contrario, el enmascaramiento no funcionará y toda la textura de agua será mostrada.

Si lo quiere que funciona en UI necesitará escribir este shader para UI. Eso defiere un poco. Las instrucciones son similares: Obtenga el shader integrado para UI y actualice la función frag, etc.

Hay muchas cosas visuales que podemos mejorar aquí, por ejemplo podemos agregar algunas partículas con enamascaramiento.

El código fuente se puede encontrar aquí.

Espero les guste leer eso y crear este efecto. Y también dejame saber como es la traducción del artículo. Estoy aprendiendo español y quiero mejorarlo. Si hay errores y algunas cosas que no entiende, por favor, póngase en contacto conmigo.

PS. Creo que probablemente los tutoriales así pueden ser más simples para entender si fueran de video tutoriales. ¿Qué piensa?

Actualización el 10 de octubre de 2020

Yo agregue una rama nueva con la implementación del caso de UI Canvas aqui. La uníca cosa aqui es yo uso Unity 2019.4.12f1 versión. Pero no debería haber cambios significativos.

Bajo construcción

Usualmente un blogger pone aquí algunos artículos relativos. Estoy trabajando en crear más contenido interesante. Tan pronto como tengo algun contenido relativo implementaré algo de lógica aquí. A continuación puede encontrar algunos comentarios. Si no hay nada, es tu oportunidad de dejar el primer comentario:) PS.Me alegra que esté aquí y lea esto^^

Sergei Alekseev

Game Developer

Discusión