Texturizado automático del terreno de Unity



En esta ocasión vamos a ver un sistema para texturizar automáticamente o proceduralmente nuestros terrenos en Unity.
Esta entrada es una continuación de la anterior: Terreno real de Google Earth en Unity.

En líneas generales, lo que pretendemos conseguir es aplicar nuestra paleta de texturas al terreno, poniendo en cada altura una textura, y luego hacer un pequeño efecto de difuminado para que el corte no sea demasiado agresivo.

La idea no es texturizar por completo el terreno, sino ahorrarnos tiempo en la capa base, para pasar directamente al nivel de detalle.

1.- Preparar el juego de texturas que vamos a utilizar

Vamos a necesitar un juego de texturas ordenadas por alturas, que son las que vamos a añadir al terreno. El script que vamos a usar texturiza el terreno de abajo a arriba usando las texturas en el orden en el que las vamos añadiendo. Por lo tanto, si queremos que la arena esté abajo, debe ser la primera textura que establezcamos.

En este caso he utilizado las texturas gratuitas del Paquete Terrain Assets de Unity Technologies.

2- Creamos el script de texturizado automático

 Vamos a acceder a los datos del terreno y vamos a ir estableciendo el "splatmap" de cada punto con la textura que queramos en función de la altura. Vamos a trabajar en la función Start. 

Para obtener los datos del terreno usamos 
TerrainData terrainData = Terrain.activeTerrain.terrainData

A continuación, y antes de comenzar el texturizado, obtenemos la altura máxima del terreno. No he encontrado un método directo de conseguirlo, por lo que toca recorrer todos los puntos del terreno e ir comparando:

         // Obtenemos altura máxima
        float max_altura = 0;
        for (int y = 0; y < terrainData.alphamapHeight; y++){
            for (int x = 0; x < terrainData.alphamapWidth; x++){
                float height = terrainData.GetHeight(y,x);
                if (height>max_altura) max_altura = height;
                }
        }

Debemos obtener un par de valores más antes de proceder. Cada punto del terreno tiene un valor que representa qué porcentaje de cada textura hay en ese punto, por lo que vamos a necesitar un array tridimensional donde guardar los valores de peso de cada textura para cada punto:

float[, ,] pTexturas= new float[terrainData.alphamapWidth, terrainData.alphamapHeight, terrainData.alphamapLayers];

Después calculamos la amplitud que tendrá cada nivel dentro del terrno. Para ello dividimos la altura máxima entre el número de texturas.

 float amplitud = max_altura/terrainData.alphamapLayers;

Ahora sí podemos entrar en materia, ya que tenemos todos los datos previos calculados. Con un doble bucle FOR, recorremos todos los puntos del terreno, y en función de la altura de cada punto, establecemos la textura para ese punto. Esta textura "base" de cada punto, tendrá un peso aleatorio entre el 30% y el 40%, y el resto del peso corresponderá a la textura anterior por altura. Con esto conseguimos algo de mezcla, aunque este proceso se puede mejorar mucho.

for (int y = 0; y < terrainData.alphamapHeight; y++){
            for (int x = 0; x < terrainData.alphamapWidth; x++){
       
                //leemos la altura del punto
                float altura = terrainData.GetHeight(y,x);
               
                 // Cálculo de la textura base
                 int textura_base = (int) Mathf.Floor(altura/amplitud);
                 if (textura_base>terrainData.alphamapLayers-1) textura_base = terrainData.alphamapLayers-1;

                 float peso = Random.Range(0.3f,0.4f);

                 pTexturas[x,y,textura_base] = peso;
                 if (textura_base<terrainData.alphamapLayers-1) pTexturas[x,y,textura_base+1] = (1.0f-peso)/2; // Comentar para evitar el mezclado
                 if (textura_base>1) pTexturas[x,y,textura_base-1] = (1.0f-peso)/2; // comentar para evitar el mezclado

            }
        }


Lo útlimo sería asignar el valor del texturizado al terreno:
terrainData.SetAlphamaps(0, 0, pTexturas);

Para probar el texturizado sin mezcla, que puede ser útil también, hacemos que float peso = 1.0f en lugar de utilizar el aleatorio, y comentamos las dos líneas que he señalado en el script.

3.- Asignar el script al terreno y ejecutar

Cuando terminemos de dejar el script a nuestro gusto (está completo al final de la entrada), lo asignamos al terreno y ejecutamos. Se disparará la función start, texturizando nuestro terreno. Una vez que lo tengamos a nuestro gusto, es muy importante no olvidar desactivar el script para que no nos machaque cualquier posible cambio que hagamos sobre el texturizado.

Me gustaría ampliar este script de dos maneras, una de ellas, poder tomar las texturas desde una carpeta en lugar de tener que usar las del terreno. Esto podría ahorrar tiempo. La otra mejora podría ser crear una ampliación del editor de Unity directamente, en lugar de usar un script añadido al terreno.

Por supuesto, conseguir  una mejor mezcla de niveles es muy interesante, y no es especialmente difícil, sólo hay que aplicarse un poco más en el interior del doble bucle for, ya que el mezclado que he hecho es muy básico.

Espero que os sirva. Aquí está el script completo:

using UnityEngine;
using System.Collections;

public class cKolmos_TexturizadoTerreno : MonoBehaviour {

    void Start () {
        TerrainData terrainData = Terrain.activeTerrain.terrainData;

         // Obtenemos altura máxima
        float max_altura = 0;
        for (int y = 0; y < terrainData.alphamapHeight; y++){
            for (int x = 0; x < terrainData.alphamapWidth; x++){
                float height = terrainData.GetHeight(y,x);
                if (height>max_altura) max_altura = height;
                }
        }

        float[, ,] pTexturas= new float[terrainData.alphamapWidth, terrainData.alphamapHeight, terrainData.alphamapLayers];


        // Establecemos la amplitud (numero de texturas partido por la altura)
       
float amplitud = max_altura/terrainData.alphamapLayers;        for (int y = 0; y < terrainData.alphamapHeight; y++){
            for (int x = 0; x < terrainData.alphamapWidth; x++){
       
                //leemos la altura del punto
                float altura = terrainData.GetHeight(y,x);
               
                 // Cálculo de la textura base
                 int textura_base = (int) Mathf.Floor(altura/amplitud);

                 if (textura_base>terrainData.alphamapLayers-1) textura_base = terrainData.alphamapLayers-1;

                 float peso = Random.Range(0.3f,0.4f);

                 pTexturas[x,y,textura_base] = peso;
                 if (textura_base<terrainData.alphamapLayers-1) pTexturas[x,y,textura_base+1] = (1.0f-peso)/2; // Comentar para evitar el mezclado
                 if (textura_base>1) pTexturas[x,y,textura_base-1] = (1.0f-peso)/2; // comentar para evitar el mezclado

            }
        }

   
        terrainData.SetAlphamaps(0, 0, pTexturas);
       

    }

}


Gracias a Duck por su aportación en UnityAnswers, donde me he basado para escribir este tutorial.

Terreno real de Google Earth en Unity
Añadir Pinceles de Terreno a Unity

Comentarios