Cargar modelos 3D en formato binario (3DS)

Anteriormente aprendimos a cargar modelos 3D  almacenados en archivos de texto en formato OBJ,  ahora vamos a cargar archivos binarios en formato 3DS, los archivos binarios son cargados más rápidamente ya que nos es necesario la conversión de texto a número, estos archivos también ocupan menos espacio en disco.

Formato Autodesk 3DS

El formato binario 3DS organiza los datos en una colección de chunck “un bloque de datos” en forma jerárquica, cada chunck tiene un ID que describe la información que el mismo contiene, también posee un tamaño que nos dice a cantidad de bytes utilizados por el chunck, seguido de los datos propios del chunck, por ejemplo: vértices, normales, índices, etc.

image

La imagen muestra algunos chunck y sub chunk que podemos encontrar en el formato 3DS, vemos el identificador, 0x4D4D es el chunck principal y contiene dos sub chunck, 0x3D3D y 0xB000 estos a su vez contienen otros chunck.

Para cargar los datos del archivo leemos los 2 primeros bytes para obtener el ID, los 4 bytes siguientes corresponden al tamaño, en este punto podemos cargar la información correspondiente,  si no nos interesa solo avanzamos al siguiente chunck.

void readChunck(std::ifstream &file, int CHUNCK_ID)
{
    uint16_t chunck = 0;
    uint32_t lenght = 0;
    
    do {
        file.read(reinterpret_cast<char*>(&chunck), 2);
        file.read(reinterpret_cast<char*>(&lenght), 4);

        if (chunck != CHUNCK_ID) file.seekg(lenght - 6, std::ios_base::cur);

    } while (chunck != CHUNCK_ID);
}

Para cargar el chunck que nos interesa debemos primero cargar el chunck padre y seguir avanzando hasta ubicarlo, cuando estemos en la posición correcta leemos los datos.

readChunck(file, 0x4D4D);
    readChunck(file, 0x3D3D);
        readChunck(file, 0x4000); readChunckChar(file);
            readChunck(file, 0x4100);
                readChunck(file, 0x4110); readChunckVertices(file, vertices);
                readChunck(file, 0x4120); readChunckFaces(file, faces);

El chunck 0x4000 contiene el nombre del objeto que vamos a cargar, para obtenerlo leemos carácter por carácter hasta encontrar ‘\0’ que marca el final de la cadena de texto, este ejemplo solo lee el texto y lo imprime en consola.

void readChunckChar(std::ifstream& file) {
    char c = ' ';
    while (c != '\0') {
        file.get(c);
        std::cout << c;
    } 
}

Para cargar los vértices que componen el modelo 3D nos ubicamos en el chunck 0x4110, leemos 2 bytes para obtener la cantidad de vértices, un máximo de 65536, luego cargamos el bloque de datos que consta de una colección de tipo float, cada vértice esta formado por 3 componentes (x, y, z), en este ejemplo cargamos todo el bloque directamente y lo almacenamos en la variable std::vector<glm::vec3> vertices.   

void readChunckVertices(std::ifstream& file, std::vector<glm::vec3>& vertices) 
{
    uint16_t num_vertices = 0;
    file.read(reinterpret_cast<char*>(&num_vertices), 2);

    vertices.resize(num_vertices);
    file.read(reinterpret_cast<char*>(&vertices[0].x), num_vertices * sizeof(glm::vec3));
}

Del mismo modo cargamos las caras del modelo, leemos el número de caras disponibles y luego almacenamos cada una de ellas, para cada cara tenemos una colección de 4 valores tipo uint16_t donde los 3 primeros indican los índices requeridos para formar la cara triangular, el cuarto valor indica como se forma la cara, por simplicidad lo omitiremos.  

void readChunckFaces(std::ifstream& file, std::vector<GLuint>& faces) 
{
    uint16_t num_faces = 0;
    file.read(reinterpret_cast<char*>(&num_faces), 2);

    faces.reserve(num_faces * 3);

    for (uint16_t i = 0; i < num_faces; i++)
    {
        uint16_t temp_faces[4];
        file.read(reinterpret_cast<char*>(&temp_faces), sizeof(short) * 4);

        faces.push_back(temp_faces[0]);
        faces.push_back(temp_faces[1]);
        faces.push_back(temp_faces[2]);
    }
}

Para poder mostrar el modelo en pantalla debemos calcular los vectores normales del mismo requeridos para cálculos de iluminación en el fragment shader, el format 3DS no almacena esta información.

for (size_t f = 0; f < faces.size(); f += 3) {

    GLuint f0 = faces[f + 0];
    GLuint f1 = faces[f + 1];
    GLuint f2 = faces[f + 2];

    glm::vec3 v0 = vertices[f0];
    glm::vec3 v1 = vertices[f1];
    glm::vec3 v2 = vertices[f2];
    
    glm::vec3 e1 = v1 - v0;
    glm::vec3 e2 = v2 - v0;
    
    glm::vec3 N = glm::cross(e1, e2);

    normals[f0] += N;
    normals[f1] += N;
    normals[f2] += N;
}

for (size_t i = 0; i < normals.size(); i++) {
    normals[i] = glm::normalize(normals[i]);
}

Con esto ya podemos mostrar el modelo en pantalla como lo hemos hecho en tutoriales anteriores, es posible también cargar las texturas, luces, cámaras, etc., lo veremos más adelante. 

opengl formato 3DS

Proyecto en GitHub: Cargar modelos en formato binario 3DS

Comentarios

Temas relacionados

Entradas populares de este blog

tkinter Grid

tkinter Canvas

Histogramas OpenCV Python

Python Binance API