Si alguna vez quisiste darle a tu juego un toque de realismo o un estilo llamativo, seguro te preguntaste cómo lograr un efecto de agua convincente. En este artículo, te contaré lo básico para crear un agua animada en Unity, incluso si jamás escuchaste hablar de “shaders” y no querés meterte en tecnicismos complejos.
¿Qué es un Shader?
En pocas palabras, un shader es un “pequeño programa” que se encarga de decirle a la computadora cómo debe dibujar algo en pantalla. Por ejemplo, qué color tendrá cada píxel, cómo se mueve la superficie de un objeto (ondas), cómo refleja la luz, etc. No necesitamos entrar en profundidad en código matemático ni nada demasiado técnico, pero sí entender que un shader es la clave para lograr efectos especiales.
Por ejemplo, podrías tener un shader que pinte todo de color rojo. Otro, que haga tu objeto transparente. Y en nuestro caso, uno que simule olas de agua.
¿Por qué no usar una simple textura de agua?
Si solo usamos una imagen de “agua” como textura, se va a ver estática. No habrá movimiento de olas ni transparencia variable. Un shader, en cambio, puede animar los vértices (para que suban y bajen) y el color (para mezclar “profundidad” y bordes claros), creando un efecto mucho más vivo.
Pasos básicos para lograr el efecto
Crear el plano de agua en Unity
En la escena de Unity, añadí un objeto de tipo Plane. Este será la superficie de tu agua.
- Asegurate de que tenga suficientes subdivisiones si querés ver olas “reales”. Por ejemplo, un plane de 10×10 subdivisiones puede funcionar, pero 20×20 o más se ve mejor.
Asignar un Material con Shader de agua
Creamos un Material (clic derecho en la ventana Project → Create → Material).
- En el Inspector, elegimos el Shader: si descargaste uno especial para agua, seleccioná “Custom/NombreDeTuShader”.
Ajustar parámetros
Dependiendo del shader que uses, vas a ver varios sliders (por ejemplo, amplitud de las olas, velocidad, color profundo, color superficial, etc.).
- Subí o bajá estos valores para conseguir más altura en las olas, mayor transparencia, un color más oscuro, etc.
Probar en modo Play
Dale al Play y mirá cómo se mueve la superficie. Ajustá en tiempo real los valores.
- Si querés olas más grandes, subí la amplitud. Si las querés rápidas, subí la velocidad.
Un ejemplo de cómo funciona internamente el agua en Unity
Aunque no entraremos en código detallado, te cuento la idea detrás de un shader de agua “básico”:
Si abrís el shader, verás algunas líneas de código que dicen “subí la posición Y del vértice en base a sin o cos”. Y en la parte de “fragment” o “pixel” (color), se decide si ese píxel será más oscuro, claro o casi transparente. Pero no te preocupes si esto suena complicado: ya hay muchos shaders listos para usar que podés importar.
Consejos para un mejor resultado
¿Ya estoy listo para hacer mi agua en Unity?
Sí. Con un plane y un shader adecuado, podés lograr un efecto muy vistoso sin complicarte. Existen varios shaders “gratuitos” en la Asset Store de Unity, o podés usar un shader que armamos aquí:
Shader "Custom/WaterShader"
{
Properties
{
_DeepColor("Deep Water Color", Color) = (0,0.25,0.8,1)
_ShallowColor("Shallow Water Color", Color) = (0.4,0.8,1,1)
_FresnelColor("Fresnel Color", Color) = (1,1,1,1)
_FresnelPower("Fresnel Power", Range(0.1,5.0)) = 2.0
_Transparency("Transparency", Range(0,1)) = 0.8
_Amplitude1("Amplitude1", Range(0,1)) = 0.1
_Frequency1("Frequency1", Range(0,10)) = 3
_Speed1("Speed1", Range(0,10)) = 1
_Direction1("Direction1 (X,Z)", Vector) = (1,0,0,0)
_Amplitude2("Amplitude2", Range(0,1)) = 0.06
_Frequency2("Frequency2", Range(0,10)) = 2
_Speed2("Speed2", Range(0,10)) = 1.3
_Direction2("Direction2 (X,Z)", Vector) = (0,1,0,0)
_Amplitude3("Amplitude3", Range(0,1)) = 0.08
_Frequency3("Frequency3", Range(0,10)) = 4
_Speed3("Speed3", Range(0,10)) = 1.8
_Direction3("Direction3 (X,Z)", Vector) = (0.7,0.7,0,0)
_FoamColor("Foam Color", Color) = (1,1,1,1)
_FoamTexture("Foam Texture (R)", 2D) = "white" {}
_FoamScale("Foam Texture Scale", Range(0,10)) = 3.0
_FoamIntensity("Foam Intensity", Range(0,2)) = 1.0
_SeaLevel("Base Sea Level", Float) = 0.0
}
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 clipPos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD2;
float waveHeight : TEXCOORD3; // altura de la ola
};
float4 _DeepColor;
float4 _ShallowColor;
float4 _FresnelColor;
float _FresnelPower;
float _Transparency;
float _Amplitude1, _Frequency1, _Speed1; float4 _Direction1;
float _Amplitude2, _Frequency2, _Speed2; float4 _Direction2;
float _Amplitude3, _Frequency3, _Speed3; float4 _Direction3;
float4 _FoamColor;
sampler2D _FoamTexture;
float _FoamScale;
float _FoamIntensity;
float _SeaLevel;
float3 GerstnerWave(float3 pos, float amplitude, float frequency, float speed, float2 direction)
{
float theta = dot(direction, pos.xz) frequency + (_Time.y speed);
float sinT = sin(theta);
float cosT = cos(theta);
float3 offset;
offset.x = amplitude cosT direction.x;
offset.z = amplitude cosT direction.y;
offset.y = amplitude * sinT;
return offset;
}
v2f vert(appdata v)
{
v2f o;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal = normalize(mul((float3x3)unity_ObjectToWorld, v.normal));
float2 dir1 = normalize(_Direction1.xy);
float2 dir2 = normalize(_Direction2.xy);
float2 dir3 = normalize(_Direction3.xy);
float3 waveOff1 = GerstnerWave(worldPos, _Amplitude1, _Frequency1, _Speed1, dir1);
float3 waveOff2 = GerstnerWave(worldPos, _Amplitude2, _Frequency2, _Speed2, dir2);
float3 waveOff3 = GerstnerWave(worldPos, _Amplitude3, _Frequency3, _Speed3, dir3);
float3 totalOffset = waveOff1 + waveOff2 + waveOff3;
worldPos += totalOffset;
o.waveHeight = worldPos.y - _SeaLevel;
o.clipPos = mul(UNITY_MATRIX_VP, float4(worldPos, 1.0));
o.worldPos = worldPos;
o.normal = worldNormal;
o.uv = v.uv;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float3 N = normalize(i.normal);
float3 V = normalize(_WorldSpaceCameraPos - i.worldPos);
float fresnelFactor = pow(1.0 - saturate(dot(N, V)), _FresnelPower);
float shallowFactor = saturate(i.worldPos.y * 0.5 + 0.5);
float3 waterColor = lerp(_DeepColor.rgb, _ShallowColor.rgb, shallowFactor);
float3 finalColor = lerp(waterColor, _FresnelColor.rgb, fresnelFactor);
float foamEdge0 = 0.02;
float foamEdge1 = 0.10;
float foamFactorHeight = smoothstep(foamEdge0, foamEdge1, i.waveHeight);
float slopeFactor = 1.0 - saturate(dot(N, float3(0,1,0)));
slopeFactor *= 0.5;
float2 foamUV = i.uv * _FoamScale;
float foamTex = tex2D(_FoamTexture, foamUV).r;
float foamFactor = foamFactorHeight + slopeFactor;
foamFactor *= foamTex;
foamFactor = saturate(foamFactor * _FoamIntensity);
float3 foamColor = _FoamColor.rgb;
finalColor = lerp(finalColor, foamColor, foamFactor);
return fixed4(finalColor, _Transparency);
}
ENDCG
}
}
FallBack Off
}
Si querés profundizar, en otro post podemos ver en detalle cómo programar esos sin(), cos() y la técnica de Gerstner Waves, cómo agregar espuma en las crestas y cómo recalcular las normales para un look más realista.
Conclusión
Crear agua en Unity es más sencillo de lo que parece si entendés que un shader básicamente “le dice” a la GPU cómo dibujar y deformar tu superficie. Con un material apropiado, un plane subdividido y unos ajustes de color, amplitud y velocidad, podés lograr un efecto de agua que se mueva y luzca genial en tu juego.
¿Te animás a probarlo? ¡Contanos cómo te fue en los comentarios!¿Querés crear tus propios juegos?
Aprendé a programar videojuegos desde cero con nuestros cursos en Unity. Más de 2,100 estudiantes ya empezaron su camino.
Ver cursos gratuitos

