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é +.