Generación procedural de modelos 3D en Unity

Generación Procedural de Terrenos con Unity

Generación procedural de modelos 3D en Unity

Cuando queremos usar un modelo 3D para nuestro videojuego hecho en Unity, podemos crearlo usando una herramienta externa como Autodesk 3DS Max, o bien usar los sencillos modelos 3D que Unity puede generar (cubo, esfera, plano, etc.). Estos modelos que Unity puede generar, se crean mediante un algoritmo que se encarga de calcular todos los vértices y las caras de dicho modelo 3D. Del mismo modo que Unity puede crear mediante programación estos modelos 3D, podemos escribir un script con C# para generar nuestros propios modelos 3D. Esto es lo que se conoce como generación procedural (o paramétrica) de modelos.

Obviamente, cuanto más complejo sea el modelo 3D, más complejo será el algoritmo para generarlo. Por lo que es importante valorar hasta que punto nos puede resultar útil, o no, generar modelos de esta forma.

Ventajas de la generación paramétrica de «assets»

Las principales ventajas que nos ofrece esta forma de trabajar son:

  • Podemos generar modelos sin necesidad de tener que abrir una aplicación externa a Unity, crear un modelo, exportarlo y luego volverlo a importar en Unity, por lo que el flujo de trabajo es más óptimo.
  • Al generarse estos modelos con un algoritmo matemático, podemos generar tantas variantes como permita el algoritmo simplemente variando los parámetros de dicho algoritmo (de ahí el nombre de generación paramétrica), e incluso generar modelos 3D bajo demanda según la interacción del jugador. Esto nos va a permitir generar infinitas variantes del mismo tipo de modelo 3D sin necesidad de modelar, con tan solo aplicar cambios a los parámetros que usa el algoritmo de generación.
  • Si al algoritmo de generación le añadimos cierta aleatoriedad, podemos hablar de generación procedural de modelos 3D. Por lo que incluso podemos hacer que los modelos generados sean hasta cierto punto aleatorios (dentro de lo que permita nuestro algoritmo de generación). Ejemplos típicos de generación procedural podrían ser la generación de terrenos o la generación de arboles.

Para lograr nuestro objetivo, necesitaremos tener los conocimientos matemáticos suficientes para poder escribir un algoritmo que nos calcule la posición de cada vértice y nos calcule los indices de cada cara de nuestro modelo 3D. Si el modelo es sencillo, por lo general, el apartado matemático será sencillo, pero si el modelo es complejo la parte matemática puede serlo también. Obviamente, junto al tiempo que implica programar el script de generación, estas son las principales desventajas de esta forma de trabajar.

Generar un terreno proceduralmente en Unity

Aunque Unity ya incluye herramientas para generar terrenos, vamos a crear nuestro propio generador (mucho más sencillo, claro). Y lo vamos a hacer, porque es una forma bastante sencilla de comprender como funciona esta forma de trabajar y las posibilidades que nos ofrece.

Primero vamos a crear un Game Object vacío (botón derecho sobre la jerarquía de objetos de la escena y seleccionamos «Create Empty»). Después, a este Game Object, le vamos a añadir los siguientes 3 componentes:

  • Mesh Filter – este componente almacenará la información del modelo 3D.
  • Mesh Renderer – este componente lo pintará en pantalla usando el material que le indicamos. Por lo que debemos de indicar un material en «Elemento 0» dentro de «Materials» o de lo contrario se utilizará el material por defecto.
  • New Script – le ponemos de nombre «GeneradorDeTerreno.cs», lo abrimos para editar y le ponemos el siguiente contenido a dicho script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
public class GeneradorDeTerreno : MonoBehaviour
{
  public int m_Seed = 0; // semilla
  public int m_Rows = 25; // número de filas
  public int m_Columns = 25; // número de columnas
  public float m_CellSize = 1.0f; // ancho de la celda
  public float m_Height = 3.0f; // altura
  public float m_NoiseScale = 0.25f; // escala del ruido
  public float m_TexCoordScale = 1.0f; // escala del mapa UV

  Vector3[] vertex; // para los vértices
  Vector2[] texcoor; // para las coordenadas de textura
  int[] index; // para las triángulos
  Mesh mesh; // nuestro modelo 3D

  void RegenerateMesh()
  {
    Random.InitState(m_Seed);
    float rdm = Random.Range(0f, 10000f);
    float uvscale = 1.0f / m_TexCoordScale;

    // generación de los vértices (posición y coordenadas de textura)
    int vtxCount = (m_Rows + 1) * (m_Columns + 1);
    vertex = new Vector3[vtxCount];
    texcoor = new Vector2[vtxCount];
    int vtx = 0;
    float x = 0f; // posición X
    float y = 0f; // posición Y
    float z = 0f; // posición Z
    for (int i=0;i<=m_Rows;i++)
    {
      x = 0f;
      for (int j=0;j<=m_Columns;j++)
      {
        // posición
        y = m_Height * Mathf.PerlinNoise(
          (x * m_NoiseScale) + rdm,
          (z * m_NoiseScale) + rdm);
        vertex[vtx] = new Vector3(x, y, z);

        // coordenada de textura
        texcoor[vtx] = new Vector2(x * uvscale, z * uvscale);

        // siguiente vértice
        vtx++;
        x = x + m_CellSize;
      }
      z = z + m_CellSize;
    }

    // generación de los indices de los triángulos
    int indexCount = m_Rows * m_Columns * 2 * 3;
    index = new int[indexCount];
    int idx = 0;
    for (int i=0;i<m_Rows;i++)
    {
      int offset = i * (m_Columns + 1);
      for (int j=0;j<m_Columns;j++)
      {
        // obtenemos los indices de los 4 vértices que forman la celda
        int a = offset + j;
        int b = a + m_Columns + 1;
        int c = b + 1;
        int d = a + 1;

        // primer triángulo
        index[idx ] = a;
        index[idx + 1] = b;
        index[idx + 2] = d;

        // segundo triángulo
        index[idx + 3] = d;
        index[idx + 4] = b;
        index[idx + 5] = c;

        // siguiente celda
        idx = idx + 6;
      }
    }

    // asignamos información al modelo 3D
    mesh.Clear();
    mesh.vertices = vertex;
    mesh.uv = texcoor;
    mesh.triangles = index;
    mesh.RecalculateNormals();
  }

  void Start()
  {
    mesh = new Mesh();
    MeshFilter mf = transform.GetComponent<MeshFilter>();
    mf.mesh = mesh;
    RegenerateMesh();
  }

}

IMPORTANTE: Para poder ver el terreno generado deberemos de pulsar el botón de Play.

Si hemos seguido los pasos correctamente, podremos observar que el terreno ha sido generado con éxito.

Terreno generado proceduralmente en Unity

Terreno generado proceduralmente en Unity

Todos los modelos 3D de Unity están formados por triángulos. Y cada triangulo viene definido por sus 3 vértices. Para cada vértice necesitamos básicamente su posición y sus coordenadas de textura (mapeado UV).

Algunos triángulos comparten vértices con otros triángulos del modelo 3D. Es por ello que para indicar cada triangulo se debe de indicar el índice de cada uno de sus 3 vértices.

En el caso del terreno que estamos generando, la malla del modelo 3D es básicamente una rejilla, tal y como se puede apreciar en la siguiente imagen:

Rejilla terreno procedural en Unity

Vista superior de una rejilla de un terreno creado con generación procedural en Unity

Lo único que diferencia esa rejilla de un terreno es que en la rejilla todos los vértices tienen la misma altura, y en un terreno la altura de los vértices va variando.

El método RegenerateMesh

Lo primero que hace este método de este script es inicializar un valor aleatorio (semilla/seed) que se usará para que el azar nos permita generar diferentes terrenos. Hay que tener en cuenta que tal y como está hecho, cada vez que le demos al Play no va a generar un terreno diferente. Para ello, antes debemos de modificar el valor de Seed en el Inspector de Unity. La idea es que un mismo valor de Seed generará siempre el mismo terreno, pero cambiando ese valor el terreno cambiará considerablemente por uno totalmente diferente. Esto es lo que diferencia un modelo generado paramétricamente de uno proceduralmente.

Después, tenemos un doble bucle for que generará filas de vértices; para cada vértice calculará la posición y las coordenadas de textura. La posición es sencilla, ya que simplemente conforme vamos añadiendo vértices nos vamos desplazando hacia la derecha, y cada vez que cambiamos de fila nos desplazamos hacia adelante (hacia arriba si estamos viendo la rejilla desde una vista superior).

Lo más difícil de entender de este código probablemente sea el cálculo de la «altura» (posición Y) de cada vértice. Para ello se usa el método Mathf.PerlinNoise() de Unity. Este método nos ira dando diferentes valores para la altura pero no lo hará simplemente de forma aleatoria, sino que son valores que siguen un patrón de «ondas» llamado ruido perlín que dan ese ese aspecto de terreno a nuestra rejilla de vértices. Todos los vértices generados se van guardando en un array.

Perlin Noise

El Perlin Noise es un tipo de ruido usado frecuentemente en la generación procedural de modelos orgánicos.

Se pueden hacer terrenos más convincentes generando una altura para el terreno global mediante un ruido perlín de baja frecuencia en sus ondulaciones (lo que serian las montañas y valles del terreno), mientras que para los detalles locales del terreno se podría usar otra altura adicional mediante otro ruido perlín de mayor frecuencia en sus ondulaciones (lo que serían las rocas, agujeros, etc. del terreno). Para ello, para calcular la altura bastaría con llamar varias veces al método PerlinNoise() con diferentes parámetros, pero en nuestro caso vamos a mantener el ejemplo sencillo y solo lo vamos a usar una única vez.

A continuación, tenemos otro doble bucle for para generar los triángulos. Una vez generados los vértices, para generar cada triangulo simplemente hemos de indicar la posición de sus 3 vértices en el array de vértices. Sin embargo, hay que tener en cuenta que los vértices del triángulo deben de indicarse siguiendo un orden clockwise. Es decir, hemos de indicar los vértices de forma que sigan el sentido de las agujas del reloj. Además, hay que tener en cuenta que para cada celda de la rejilla necesitamos un par de triángulos que comparten un par de vértices. Por lo que tenemos 4 vértices por celda a los que podemos llamar a, b, c y d.

Orden de los vertices de un quad en Unity

Los 4 vértices y 2 triángulos de un quad en Unity

Tal y como se puede observar en la anterior ilustración, cada celda de nuestro terreno está formada por 2 triángulos que comparten 2 vértices. Los vértices a y d pertenecen a la misma fila de vértices generados en el primer paso, mientras que los vértices b y c pertenecen a la siguiente fila de vértices. Una vez averiguados los valores de los indices de dichos vértices, para indicar el triangulo azul debemos de indicar los valores de a, b y d, y hacerlo además en dicho orden (sentido de las agujas del reloj). También podríamos indicar los valores de b, d y a, o los valores de d, a y b; lo importante es respetar el sentido de las manecillas del reloj. Lo mismo se aplica para el triangulo rojo, podemos elegir cualquiera de sus 3 vertices para empezar, pero el orden ha de ser el de las manecillas del reloj.

Una vez tenemos listos los arrays con las posiciones de los vértices, las coordenadas UV para el mapeado de textura y los indices de los vértices que forman los triángulos, tan solo hay que indicárselos al objeto Mesh que tenemos en el Mesh Filter. Como no hemos calculado las normales de cada vértice, con el método RecalculateNormals() Unity las calculará por nosotros. Para ello simplemente calculará la normal de cada triangulo al que pertenece cada vértice, las sumará, y el vector resultante lo normalizará. En el caso del terreno que estamos generando, estas normales generadas por Unity son adecuadas, pero en otro tipo de modelo 3D, tal vez sea mejor generar las normales «a mano». La normal de un vértice indica hacia donde apunta dicho vértice y es muy importante a nivel de iluminación.

Y nada más, espero que el código sea lo bastante claro y os animo a generar otro tipo de modelos 3D según vuestras necesidades mediante generación procedural.

 

 

Escribe un comentario