Cargando

Peleando con el nuevo mar en Master of Cladia

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

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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));

  1. 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;

  1. 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. 😉

Comentarios (0)

Deja un comentario

Noticias Populares