Mar 06 2007

MD3DM: Como ler um arquivo no formato OBJ (Wavefront)

Autor: Marcos Dell Antonio - Categorias: .NET, Direct3D, Mobilidade

Finalmente cheguei na parte interessante do Trabalho de Conclusão de Curso: ler um arquivo no formato OBJ e renderizá-lo na tela do celular.

Antes de chegar até este ponto, estudei desde o início o funcionamento da API Managed Direct3D Mobile. Até agora ela está se saindo muito bem, apesar da falta de documentação (compensada pelos documentos sobre o irmão mais velho, Managed DirectX).

Em vários sites encontrei a definição do formato OBJ, portanto me parece que é bem aceitado entre a comunidade de desenvolvedores. No entanto, até agora não achei uma implementação em C# para trabalhar com ele. Logo, parti do princípio: criar uma classe, ler o arquivo, carregar algumas informações na memória e renderizá-las.

Com a ajuda do usuário Punkoff (é só o que sei sobre ele) do fórum MSDN, consegui tomar um excelente rumo no desenvolvimento do trabalho. Ele me ajudou em vários momentos onde estava pensando em chutar o balde. Acredito que ele não lerá isto, mas de qualquer forma fica aqui a minha gratidão ao tempo dedicado a um desconhecido.

A implementação que fiz é algo bem simples. Antes de descrevê-la, vou explicar rapidamente como funciona um arquivo no formato OBJ.

Este formato de arquivo é o tão conhecido “texto puro”, ou seja, dá pra abrir com o bloco de notas ou qualquer outro editor simples. No meu ponto de vista, é muito mais fácil trabalhar com arquivos deste tipo do que com os binários, pois a legibilidade do conteúdo é essencial para os iniciantes entenderem exatamente o que estão fazendo.

Dentro de um arquivo OBJ pode-se encontrar várias “coisas”, dentre elas:

- comentários: são linhas que começam com o caracter #. Exemplo:

# Isso é um teste

- definições de vértices: são linhas que começam com a letra v seguida de três coordenadas: x, y e z. Exemplo:

v -0.5 -0.5 0.0

Por enquanto só vou tratar destes dois elementos, que é o suficiente para desenhar um objeto na tela.

O arquivo OBJ que usei durante os testes contem o seguinte:

# Simple Wavefront file
v -0.5 -0.5 0.0
v 0.0 0.5 0.0
v 0.5 -0.5 0.0

Através destas três coordenadas é possível desenhar um triângulo para ilustrar a rotina que foi desenvolvida. Falando nela:

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Drawing;
using System.Reflection;
using Microsoft.WindowsMobile.DirectX;
using Microsoft.WindowsMobile.DirectX.Direct3D;
using System.Windows.Forms;

namespace LoadOBJ
{
    public class ObjLoader
    {
        private Device _device = null;
        private StreamReader _streamReader = null;
        private List<string> _vertices = null;
        private List<string> _faces = null;

        /// <summary>
        /// Constructor
        /// </summary>        
        public ObjLoader(Device device, string file)
        {
            // Set the device
            _device = device;

            // Create the stream reader
            Stream stream =
                Assembly.GetExecutingAssembly().
                    GetManifestResourceStream(file);
            _streamReader = new StreamReader(stream);

            // Load the data to fill the lists and
            // another informations about the model
            LoadData();
        }

        /// <summary>
        /// Load the data to fill the lists and another
        /// informations about the model
        /// </summary>
        /// <remarks>
        /// Just will load those lists that are null
        /// to avoid overhead
        /// </remarks>
        private void LoadData()
        {
            bool loadVertices = (_vertices == null);
            if (loadVertices)
                _vertices = new List<string>();

            bool loadFaces = (_faces == null);
            if (loadFaces)
                _faces = new List<string>();

            if (loadVertices || loadFaces)
            {
                string line;

                while
                ((line = _streamReader.ReadLine()) != null)
                {
                    // Ignore comments
                    if (line.StartsWith("#"))
                        continue;

                    // Vertex
                    if (loadVertices && line.StartsWith("v"))
                        _vertices.Add(line);

                    // Face
                    else if (loadFaces && line.StartsWith("f"))
                        _faces.Add(line);
                }
            }
        }

        /// <summary>
        /// Load the model
        /// </summary>
        public void Load(out VertexBuffer vertexBuffer)
        {
            vertexBuffer = GetVertexBuffer();
        }        

        /// <summary>
        /// Return a Vertex Buffer
        /// </summary>
        private VertexBuffer GetVertexBuffer()
        {
            VertexBuffer result =
                new VertexBuffer(
                    typeof(CustomVertex.PositionColored),
                    _vertices.Count,
                    _device,
                    Usage.None,
                    CustomVertex.PositionColored.Format,
                    Pool.SystemMemory);

            result.Created +=
                new EventHandler(OnVertexBufferCreate);
            OnVertexBufferCreate(result, null);

            return result;
        }

        /// <summary>
        /// Handle that is executed when the
        /// vertex buffer is created
        /// </summary>
        private void OnVertexBufferCreate(object sender, EventArgs e)
        {
            VertexBuffer buffer = (VertexBuffer)sender;
            buffer.SetData(GetVerticesArray(), 0, LockFlags.None);
        }

        /// <summary>
        /// Return an array of vertices
        /// </summary>
        private CustomVertex.PositionColored[] GetVerticesArray()
        {
            CustomVertex.PositionColored[] result =
                new CustomVertex.PositionColored[_vertices.Count];
            for (int i = 0; i < _vertices.Count; i++)
            {
                float x, y, z;
                LoadXYZ(_vertices[i], out x, out y, out z);

                result[i] =
                    new CustomVertex.PositionColored(
                        x,
                        y,
                        z,
                        Color.Black.ToArgb());
            }

            return result;
        }

        /// <summary>
        /// Load x, y and z from line into the params
        /// </summary>        
        private void LoadXYZ(string line,
            out float x, out float y, out float z)
        {
            string[] text = Regex.Split(line, " ");

            // text[0] = v; text[1] = x; text[2] = y; text[3] = z
            x = float.Parse(text[1]);
            y = float.Parse(text[2]);
            z = float.Parse(text[3]);
        }
    }
}

Para usá-la:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.WindowsMobile.DirectX;
using Microsoft.WindowsMobile.DirectX.Direct3D;

namespace LoadOBJ
{
    public partial class Form1 : Form
    {
        private Device _device;
        private VertexBuffer _vertexBuffer;

        static void Main()
        {
            Form1 form = new Form1();
            form.InitializeGraphics();
            try
            {
                Application.Run(form);
            }
            catch (Exception ex)
            {
                MessageBox.Show("
                    Erro ao iniciar a app: " + ex.Message);
            }
        }

        private void InitializeGraphics()
        {
            try
            {
                PresentParameters presentParams =
                    new PresentParameters();
                presentParams.Windowed = true;
                presentParams.SwapEffect = SwapEffect.Discard;

                // Device
                _device = new Device(
                    0,
                    DeviceType.Default,
                    this,
                    CreateFlags.None,
                    presentParams);
                _device.RenderState.CullMode = Cull.None;
                _device.RenderState.Lighting = false;

                // Load the model
                ObjLoader obj = new ObjLoader(
                    _device,
                    "LoadOBJ.Ex1.obj");
                obj.Load(out _vertexBuffer);
            }
            catch (Exception ex)
            {
                MessageBox.Show("Erro na inicialização: " +
                    ex.Message);
            }
        }

        public Form1()
        {

        }

        protected override void OnPaintBackground(PaintEventArgs e)
        {
            //
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            // Render
            Render();

            // Invalidating the window will cause it to
            // be redrawn in the future
            Invalidate();
        }

        /// <summary>
        /// Render the data
        /// </summary>
        private void Render()
        {
            // Clear the device
            _device.Clear(ClearFlags.Target, Color.White, 1.0f, 0);

            // Begin the scene
            _device.BeginScene();

            // Set stream buffer
            _device.SetStreamSource(0, _vertexBuffer, 0);
            _device.DrawPrimitives(
                PrimitiveType.TriangleList,
                0, 1);

            // End the scene and present
            _device.EndScene();
            _device.Present();
        }
    }
}

Não gosto de colar todo o código fonte num post, pois acaba deixando ele muito extenso e chato para ler. No entanto, é com base neste novo protótipo que vou escrever os próximos posts.

Bem rapidamente, o que isso tudo faz?

Muita coisa é exatamente igual ao que eu já postei há alguns dias. A única alteração é de onde virá a informação a ser desenhada.

A classe ObjLoader possui um constructor e um método chamado Load(). A partir deles é possível carregar um modelo especificado através de vértices. No código acima, comecei alguma implementação para faces, porém está incompleta, pois para utilizá-la vou precisar de um IndexBuffer também.

O resultado final é o mesmo apresentado há dias, só que desta vez usando o Windows Mobile 6.0 SDK (com o 5.0 dá pra fazer extamente a mesma coisa sem alterar o código acima) e um arquivo OBJ. Veja:

A definição de um arquivo OBJ pode ser encontrada aqui, aqui ou aqui.

É isso ae! Até +.

Adicione ao del.icio.us del.icio.us | Adicione ao Rec6 Rec6

Faça um comentário