Buenas Alquimistas,
Cuesta dejar las cosas bonitas…
para ilustrar lo q nos está costando, os vamos a explicar, con la ayuda de Codex, todas las técnicas que hemos estado usando de cara a poner un mar realista y optimizado. Ahí van:
Técnicas de renderizado de agua en Unity (Built-in RP) — Master of Cladia
- Mesh subdividido con Vertex Colors de orilla
El problema: Un quad de 4 vértices no permite vertex displacement. La solución: Grid de 128×128 segmentos (16.641 vértices). En la generación del mesh, para cada vértice se llama a terrain.SampleHeight y se calcula un valor shore (0=mar abierto, 1=orilla) que se almacena en el canal R del vertex color. Ese dato viaja al shader gratis y permite modular cualquier efecto por zona.
- UV Scroll → UV Wobble (bamboleo)
El problema: El scroll lineal clásico (uv = velocidad × tiempo) mueve la textura siempre en la misma dirección → el ojo lo lee como corriente de agua. La solución: Reemplazar por oscilación sinusoidal (uv = sin(tiempo × freq) × amplitud). La textura oscila adelante-atrás sin avanzar nunca → sin efecto de corriente.
- Ondas omnidireccionales (suma de senos)
El problema: Dos senos direccionales dan un bamboleo con dirección dominante. La solución: Sumar 4 senos a 0°, 45°, 90° y 135° con frecuencias ligeramente distintas. Las direcciones se cancelan entre sí → el agua sube y baja localmente sin fluir → efecto de lago en calma.

- Gerstner Waves
El estándar de la industria para olas realistas. La diferencia clave con senos simples:
- Seno simple: los vértices solo se mueven en Y → parece un suelo vibrante
- Gerstner: los vértices se mueven en XYZ → crestas afiladas y valles anchos, como el agua real
Fórmula por ola:
float f = k * dot(dir, posXZ) - t;
offset.x = Q * a * dir.x * cos(f); // desplazamiento horizontal
offset.z = Q * a * dir.y * cos(f);
offset.y = a * sin(f); // desplazamiento vertical
Con Q (steepness) controlas la agudeza de la cresta. 4 olas en distintos ángulos y frecuencias dan un patrón omnidireccional convincente. Performance: prácticamente idéntica a senos simples.
- Alpha fade de orilla
El problema: El mesh de agua corta el terrain con una línea dura y artificial. La solución: Usar el vertex color shore en el fragment shader con smoothstep para reducir el alpha progresivamente cerca de la orilla. El agua se vuelve transparente donde toca la playa — la línea desaparece. El depth test (terrain opaco escrito antes en Z-buffer) descarta correctamente los píxeles sobre tierra sin coste adicional.
col.a = _Color.a * (1.0 - smoothstep(_ShoreFade, 1.0, i.shore));
- Normal Map Iluminación propia
En lugar de depender de las luces de escena (_LightColor0 puede ser negro), el shader incluye su propia dirección de sol configurable en el material.
- Half-Lambert para difuso suave sin zonas completamente negras: dot(N,L) * 0.5 0.5
- Blinn-Phong normalizado para especular: factor (n 8)/8π garantiza que el brillo pico sea constante independientemente del Shininess — sin normalizar, subir el slider hace desaparecer el reflejo en vez de reducir solo su tamaño
float normFactor = (_Shininess 8.0) / 25.13274;
float spec = pow(NdotH, _Shininess) * normFactor * _SpecularStr;

- Generador de textura Perlin noise (en Unity Editor)
Script C# que genera en el Editor (vía [ContextMenu]) dos PNG tileleables:
- Diffuse: ruido multi-octave con contraste ajustable
- Normal map: calculado por diferencias centrales del mapa de alturas, con wrap para mantener el tileado seamless
El truco del tileado perfecto: mapear las coordenadas UV a un toroide 4D antes de samplear Perlin:
float nx = Mathf.Cos(u * PI2) * freq;
float ny = Mathf.Sin(u * PI2) * freq;
float nz = Mathf.Cos(v * PI2) * freq;
float nw = Mathf.Sin(v * PI2) * freq;
sample = (PerlinNoise(nx, nz) PerlinNoise(ny, nw)) * 0.5f;
Técnicas descartadas y por qué
|
Técnica |
Por qué no funcionó |
|
Depth map por SampleHeight |
La zona superficial quedaba tapada por el terrain |
|
Depth map por distancia hexagonal |
Bug de offset de coordenadas (terrain en Z≠0), mucho tiempo perdido |
|
Espuma con textura shader |
El visual no convencía |
|
Animación "ola que avanza a orilla" |
Varios intentos de fórmulas, ninguno quedó convincente |
|
Plains1_h.psd como textura de agua |
Era un heightmap con curvas de nivel topográficas — se veían en el agua |
Lecciones aprendidas
- Shaders Unity Built-in: ningún carácter no-ASCII en el código — la →, la — y las tildes dentro del bloque CGPROGRAM dan parse error
- Blinn-Phong sin normalizar: tiene un rango útil de Shininess ridículamente estrecho (~4–10). Con normalización el rango se amplía a 8–128
- Amplitud de olas vs altura de cámara: 8cm de ola es invisible desde 50m de altura. Para una cámara de estrategia se necesita 1–3m de amplitud
- Los normal maps heredan los patrones de la textura origen — no usar heightmaps de terreno como base para normal maps de agua
- Performance móvil: el cuello de botella real es el fill rate (píxeles transparentes), no el vertex shader. Bajar subdivisionesAgua de 128 a 64 reduce los vértices a 1/4 sin pérdida visual apreciable

Y por supuesto os pasamos un montón de las imágenes que hemos estado sacando de todos nuestros intentos de “mar bonito”.
¿Os animáis a hacer un mar bonito?. Mandadnos fotos de vuestros intentos. 😉