Mar 06 2007
MD3DM: Como ler um arquivo no formato OBJ (Wavefront)
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:

É isso ae! Até +.







