top of page
Foto del escritorBraulio Madrid

Marching cubes, segunda parte.




En el blog anterior habíamos hecho la parte de modelado en #Blender, esta vez cubriremos la parte de #scripting, advierto que no soy un experto en la materia, mi intención es mostrar como aplicar un algoritmo procedural para la creación de terreno, por lo tanto será algo básico.


Preparación de la escena.


  • Vamos a importar el archivo FBX que creamos en #Blender en el Blog pasado.

  • Vamos a retirar todas las animaciones, materiales, luces, cámaras y blend shapes.

  • Creamos una nueva escena y arrastramos el modelo a la escena.

  • Seleccione todos los objetos hijos y elimine el componente Mesh Renderer.

  • Arrastramos cada uno de los objetos hijos del modelo a alguna carpeta a la ventana de proyecto para crear prefabs.

  • Elimine el modelo principal de la escena.

  • Cree un cubo y agréguele un componente que llamaremos: IntelligentCube.cs.

  • Cree otro objeto vacío y agréguele un componente llamado CubeGenerator.cs.

Construcción del cubo inteligente.


Lo primero y más importante es lograr hacer que nuestro cubo elija la apariencia necesaria según los cubos vecinos que lo rodeen.


El objeto cubo, debe contener un componente Mesh Filter, Mesh Renderer y Box Collider, este script aprovecha el Box Collider para ser detectado por otros cubos y usa Mesh Filter para cambiar la malla según los vecinos que lo rodeen.


using UnityEngine;

public class IntelligentCube: MonoBehaviour {

    public MeshFilter mesh;
    private MeshFilter selectedMesh;
    
    public bool u;
    public bool d;
    public bool r;
    public bool l;
    public bool f;
    public bool b;


    // Use this for initialization
    void Start () {
        mesh = GetComponent<MeshFilter>();
        transform.position = Vector3Int.FloorToInt(transform.position);
        Vector3 origin = Vector3Int.FloorToInt(transform.position);
        Ray up = new Ray(origin, Vector3.up);
        Ray down = new Ray(origin, Vector3.down);
        Ray right = new Ray(origin, Vector3.right);
        Ray left = new Ray(origin, Vector3.left);
        Ray forward = new Ray(origin, Vector3.forward);
        Ray back = new Ray(origin, Vector3.back);

        u = Physics.Raycast(up,1);
        d = Physics.Raycast(down,1);
        r = Physics.Raycast(right,1);
        l = Physics.Raycast(left,1);
        f = Physics.Raycast(forward,1);
        b = Physics.Raycast(back,1);

        //"LRDUBF"
        string meshString = l ? "L" : "0";
        meshString += r ? "R" : "0";
        meshString += d ? "D" : "0";
        meshString += u ? "U" : "0";
        meshString += b ? "B" : "0";
        meshString += f ? "F" : "0";

        gameObject.name = meshString;

        if (meshString == "000000") Destroy(this.gameObject);
        if (meshString == "LRDUBF")
        {
            Destroy(mesh);
            Destroy(GetComponent<MeshRenderer>());
        }

        string path = "Prefabs/" + meshString;

        if (Resources.Load<MeshFilter>(path)) selectedMesh = Resources.Load<MeshFilter>(path);
        else Debug.Log("No hay Mesh en: " + path);

        if(mesh)mesh.mesh = selectedMesh.sharedMesh;
    }
	
	public void OnDrawGizmosSelected() {
        transform.position = Vector3Int.FloorToInt(transform.position);
        Gizmos.color = new Color(0,0.5f,1,0.5f);
        Gizmos.DrawCube(transform.position, Vector3.one);
        
	}
}

Para detectar los cubos vecinos, lanzamos rayos en las direcciones de cada cara y según si detecta una colisión asignamos una letra u otra, cada letra corresponde a cada cara para conformar una "palabra" de 6 letras. Si recuerdan en la primera parte cuando creamos cada uno de los cubos, los nombres de estos cubos venía asociado a los lados destapados de cada cubo.


Si la palabra fue "000000", entonces destruimos todo el objeto, porque esto quiere decir que el cubo no tiene vecinos.


Si la palabra fue "LRDUBF", entonces destruimos la malla, porque al estar rodeado de vecinos la malla del objeto es como si no tuviera nada.


Por último usamos la ruta de la carpeta donde están los distintos cubos y la palabra o string correspondiente para cargar de la carpeta "Resources" la malla del cubo correspondiente.


El resto en OnGizmosDrawSelected, es para que el objeto vacío tenga alguna forma visual para nosotros poder manipularlo.


Construcción del generador de cubos.


Ya hemos creado un cubo que toma la forma debida respecto a los vecinos, pero ahora necesitamos algo que genere más de estos cubos y los ponga de una forma que tenga algun sentido.


Hay que tener en cuenta que debe haber un límite, porque computacionalmente es pesado los bucles, si tienes un size de 8, pues es fácil, en estos bucles anidados eso equivale a 8x8x8 = 512 operaciones, pero si el size es de 32, eso es 32^3 = 32,768 operaciones y esto comienza a ser un problema de rendimiento.


Para tratar de darle un sentido, uso la función PerlinNoise 2D que tiene la clase Mathf de unity para determinar según el valor si crear un cubo inteligente o no en ese punto, el problema es que estoy tratando de convertir la función 2D en una función 3D y creo que salió mal.


Algo a tener en cuenta es que los valores deben ser flotantes, por alguna razón esta función es muy sensible cuando uno de los datos es un número entero.

using UnityEngine;

public class MarchingCubeChunk : MonoBehaviour {

    public GameObject intelligentQbe;
    public int size;
    public float scale;
	// Use this for initialization
	void Start () {

        Vector3Int initialPosition = Vector3Int.FloorToInt(transform.position);
        transform.position = initialPosition;

        for (int y = 0; y < size; y++)
        {
            for (int z = 0; z < size; z++)
            {
                for (int x = 0; x < size; x++)
                {
                    float xCoord = ((float)initialPosition.x + (float)x / (float)size) * scale;
                    float yCoord = ((float)initialPosition.y + (float)y / (float)size) * scale;
                    float zCoord = ((float)initialPosition.z + (float)z / (float)size) * scale;
                    float noise = Mathf.PerlinNoise(xCoord, zCoord);
                    noise = Mathf.PerlinNoise(noise, yCoord);
                    //Debug.Log(noise);
                    if (noise <= 0.5f) intelligentQbe = Instantiate(intelligentQbe, initialPosition + new Vector3Int(x, y, z), Quaternion.identity, transform);
                }
            }
        }
	}

    // Update is called once per frame
    public void OnDrawGizmosSelected()
    {
        transform.position = Vector3Int.FloorToInt(transform.position);
        Gizmos.color = new Color(0, 0.5f, 1, 0.5f);
        Gizmos.DrawCube(transform.position + Vector3.one * (size / 2), Vector3.one * size);
    }
}

El resto, la función OnDrawGizmosSelected, es solo para visualizar algo en los objetos vacíos y poder ubicarlo más fácil.


No siendo más esto es todo, traté de simplificar un tema complejo en algo más sencillos de digerir, tiene errores y aunque el enfoque es parecido, dista mucho de lo que debe ser un marching cubes.


#MarchingCubes usa #metaballs, que es otro algoritmo que crea esferas que se unen unas con otras como gotas de agua, como desconozco el algoritmo, no pude presentarles una opción más acorde.


Puede que los cubos dejen muchos rotos, pero algunos de estos problemas puede deberse a un mal nombre dentro o pulir algunos bordes, este algoritmo tampoco tiene en cuenta las diagonales y por lo tanto tampoco puede elegir un cubo que encaje mejor.


Eso es todo por esta ocasión, nos vemos en el siguiente Blog.


7 visualizaciones0 comentarios

Comments


bottom of page