Эта публикация перенесена в раздел уроков по адресу Загрузка .X (DirectX) файлов в OpenGL. К ней прикреплена новая отдельная ветка комментариев форума, которую вы можетет найти после текста публикации. Обсуждение публикации рекуомендуется вести по новому адресу, который указан выше.
Для загрузки из файла трехмерных объектов (Meshs) в OpenGl , нет встроенных решений. Так, как OpenGL- это кросс-платформенная библиотека, которая была написана для вывода графики, а не для работы с файловой системой, потоками....
Поэтому хочу поведать, как грузить удобный, и простой для понимания загрузки, DirectX-совский .X файл. Мы будем использовать связку C# + Tao.Framework, поэтому желательно почитать пару уроков, как ее использовать.
[spoiler] Небольшое отступление
Кстати, после установки Tao все библиотеки из "C:\Program Files\TaoFramework\lib" нужно скопировать в "C:\Windows\System32", иначе библиотек не будет в системе. И Visual Studio не будет их видеть.
Файлы трехмерных моделей в формате .X
.X файл - формат файла для хранения 3D объектов (Meshs). Может быть,как текстовый ".x",так бинарный ".bin".
Этот формат хранит информацию 3D объекта:
- координаты вершин(Vertex), последовательность их загрузки(Face); - координаты нормалей(Vertex), последовательность их загрузки(Face); - текстурные координаты, описание материалов, названия текстур.
Vertex - это точка в трехмерном пространстве, содержащая в себе координаты X Y Z. Normals - это вектор направленный вверх от полигона, предназначенный для расчета света. Полигон - это простая фигура сложенная в трехмерном пространстве из Vertex-ов.(например: триугольник, прямоугольник)
Вот выглядит простой куб в .X файл экспортированный CyberMotion 3D-Designer (v12.0):
Видя структуру файла, можно понять какие слова являются ключевыми и, как граничатся записи данных.
Я создавал загрузчик под определенную программу, а именно CyberMotion 3D-Designer, но CyberMotion 3D-Designer не удобен в плане необходимости лицензии. Для того, чтобы не было мороки с малоизвестной всем программой, я модифицировал загрузчик для большей универсальности. Так, что он сможет грузит .x файлы экспортированные через Blender.
Blender бесплатный, его легко найти и скачать, он работает с многими известными форматами, и по нему много разной инфы и сайтов.
Единственное, что может показаться недостатком, так это то что придется установить Python(точнее его интерпретатор). Сразу хочу предупредить, что этой мой пробный загрузчик и не исключены ошибки и малопроизводительные решения, а точнее должны быть)). Но как говориться: "нет программ без ошибок, просто плохо искали"))).
Код загрузчика
Приступим к делу. Сперва создаем проект оконного приложения С#. - Добавим в references(ссылки) Tao.FreeGlut, Tao.DevIl, Tao.OpenGl, Tao.Platform.Windows
- Подключим библиотеки (директива using):
/*http://esate.ru, Flashhell*/
using System.IO;
using Tao.FreeGlut;
using Tao.OpenGl;
using Tao.Platform.Windows;
using Tao.DevIl;
- Прописуем создания класса прям в Form1(или как Вы его назвали).cs
/*http://esate.ru, Flashhell*/
public partial class Form1 : Form
{}
public class mesh
{}
- Добавим целую кучи глобальных переменных
/*http://esate.ru, Flashhell*/
public class mesh
{
int list;
string[,] MaterialName;
Mesh[] Meshs;
StreamReader sw;
int imageId;
int count;
int schetMaterial = 0;
int kolMaterial = 0;
uint mGlTextureObject = 0;
short kolObject = 0;
public string buf = "";
}
- Первое, что необходимо нашему классу (кроме глобальных переменных)) - это функция для получения первого слова в строке, а потом уже от полученного слова будут работать другие функции. Здесь можно, и лучше использовать вариант функции с класса загрузки .ASE файлов anModelLoader, но я решил написать свою функцию))) какую использовать решать Вам:
/*http://esate.ru, Flashhell*/
private string GetFirstWord(string word)
{
if (word == "") return "";//если строка пуста, то выходим из функции
int start = 0;//с этого(пока нулевого) символа начнем вырезку начальных пробелов
while(word[start]==' ')// если пробелы есть считаем до первой буквы
{
start++;
}
if(start>0)//если пробелы есть
{
word = word.Substring(start,(word.Length-start));//отрезаем пробелы
}
string[] s = word.Split(' ');//split-ом делим на подмассивы с разделителем пробел = ' ',
//чтобы получить массив слов без пробелов
return s[0];//возвращаем первый элемент массива, то есть первое слово
}
- Теперь когда есть базовая функция, можем приступить функции определения ключевых слов:
/*http://esate.ru, Flashhell*/
public bool Read()
{
string temp = sw.ReadLine(); //читаем строку из потока, поток(sw) мы создадим в другой функции.
if (temp == null) //если ничего не зачитали с потока, выходим из функции
return false;
string buf2 = temp; // записуем полученную строку, чтоб при необходимости к ней обратиться
//buf = temp;
temp = GetFirstWord(temp); //получаем первое слово в строке и перезаписуем temp
switch (temp) // проверяем на совпадение с ключевыми словами
{
case "Mesh":
count++;
LoadMesh(); /*каждому ключевому слову будет
соответствовать функция загрузки определенного параметра объекта,каждую из,
которых мы вскоре рассмотрим*/
break;
case "MeshNormals":
LoadNormals();
break;
case "MeshTextureCoords":
LoadTextureCoords();
break;
case "Material":
LoadMaterial(buf2,false);
break;
case "MeshMaterialList"://сложные и мало понятные манипуляции(объя)
// для получения соответствующего Material-а для объекта
string tempo = "";
string str;
bool flag = false;
while (flag == false)
{
str = sw.ReadLine();
if (GetFirstWord(str) == "Material")
{
LoadMaterial(str,true);
}
if (str == @"}")
break;
string word = GetFirstWord(str);
if (word == @"}")
break;
for (int i = 0; i < kolMaterial; i++)
{
tempo = MaterialName[i, 0];
tempo = @"{" + tempo + @"}";
if (tempo == str)
{
Meshs[count].fileTexture = @"путь к текстурам" + MaterialName[i, 1];
/*
используйте следующие методы получения пути исполняемого файла,
чтобы к ниму прикрепить папку с файлами текстур
System.Reflection.Assembly assem = System.Reflection.Assembly.GetEntryAssembly();
string path = Path.GetDirectoryName(assem.Location);
*/
flag = true;
break;
}
}
}
break;
}
return true;
}
- напишем функцию открытия файла:
/*http://esate.ru, Flashhell*/
public void Open(string url)
{
sw = new StreamReader(url, Encoding.Default);//открываем поток чтения файла
string temp;
while ((temp = sw.ReadLine()) != null)//считаем количество объектов и материалов для [B]выделения на них памяти[/B]
{
temp = GetFirstWord(temp);
if (temp == "Mesh")
kolObject++;
if (temp == "Material")
kolMaterial++;
}
sw.Close();
Meshs = new Mesh[kolObject];//выделяем память
MaterialName = new string[kolMaterial, 2];
count = -1;
sw = new StreamReader(url, Encoding.Default);// снова открываем уже прочтенный поток
}
- создаем конструкторы класса
/*http://esate.ru, Flashhell*/
public mesh()
{
}
public mesh(string url)//основной конструктор для открытия и чтения
{
Open(url);
while (Read() == true)//читаем по строке пока поток не кончиться
{
}
DrawInMemory();
}
- теперь отдельные функции для загрузки определенных ключевыми словами(Mesh, MeshNormals, Material, MeshTextureCoords) данных и вложенных в них данных(Vertex, Face), но сначала создадим структуры куда будем это все записывать
структуры:
/*http://esate.ru, Flashhell*/
public struct Mesh
{
public string fileTexture;
public Vertex[] MeshVertex;
public int[] MeshFace;
public Vertex[] MeshNormals;
public int[] MeshFaceNormals;
public float[,] TextCoords;
}
public struct Vertex
{
private float xx, yy, zz;
public Vertex(float x, float y, float z)
{
xx = x;
yy = y;
zz = z;
}
#region prop
public float X
{
get
{
return xx;
}
set
{
xx = value;
}
}
public float Y
{
get
{
return yy;
}
set
{
yy = value;
}
}
public float Z
{
get
{
return zz;
}
set
{
zz = value;
}
}
#endregion
}
функции:
/*http://esate.ru, Flashhell*/
private string LoadFileNameTexture(string StrTextureFilename)//функция загрузки имени файла текстуры
{
string[] temp = StrTextureFilename.Split('\"');//имя файла взято в кавычки, поэтому делим на массивы отталкиваясь от этого
return temp[1];//возвращаем второй элемент массива, тоесть, то что за первой кавычкой и до последней
}
private string LoadNameMaterial(string str)//функция загрузки имени Material-а
{
int start = 0;// тоже принцип что и в [B]GetFirstWord[/B]
while (str[start] == ' ')
{
start++;
}
if (start > 0)
{
str = str.Substring(start, (str.Length - start));
}
string[] temp = str.Split(' ');
return temp[1];// только возращаем второе слово
}
private void LoadMesh()//функция загрузки [B]Mesh[/B]-а
{
string temp = sw.ReadLine();
int kol = Convert.ToInt32(temp.Replace(";", ""));//определяем количество строк данных
Meshs[count].MeshVertex = new Vertex[kol];//выделяем память под Vertex-ы подобъекта
LoadVertex(Meshs[count].MeshVertex);// вызываем функцию загрузки Vertex-сных(Геометрических) данных
while ((temp = sw.ReadLine()) == null || temp == "")//пропускаем пустые строки
{
}
kol = Convert.ToInt32(temp.Replace(";", ""));//определяем количество строк данных
Meshs[count].MeshFace = new int[kol * 3];//выделяем память под Face подобъекта
LoadFace(Meshs[count].MeshFace, kol);//вызываем загрузку Face-ов
}
private void LoadNormals()
{
string temp = sw.ReadLine();
int kol = Convert.ToInt32(temp.Replace(";", ""));//определяем количество строк данных
Meshs[count].MeshNormals = new Vertex[kol];//выделяем память
LoadVertex(Meshs[count].MeshNormals);// вызываем функцию загрузки Vertex-сных(Геометрических) данных
while ((temp = sw.ReadLine()) == null || temp == "")//пропускаем пустые строки
{
}
kol = Convert.ToInt32(temp.Replace(";", ""));
Meshs[count].MeshFaceNormals = new int[kol * 3];//выделяем память
LoadFace(Meshs[count].MeshFaceNormals, kol);//вызываем загрузку Face-ов
}
private void LoadTextureCoords()
{
string temp = sw.ReadLine();
int kol = Convert.ToInt32(temp.Replace(";", ""));//определяем количество строк данных
Meshs[count].TextCoords = new float[kol, 2];//выделяем память
LoadCoords(Meshs[count].TextCoords);//вызываем загрузку текстурных координат
}
private void LoadVertex(Vertex[] MeshVertex)
{
/*так, как память мы выделяли под просчитанный размер, будем
считывать до конца выделенной памяти
Пример записи [B]Вертексов[/B]: -50.0;13.895662;-50.0;, */
for (int i = 0; i < MeshVertex.Length; i++)
{
// заменяем точки в представлении числа с плавающей точкой,
// на запятые, чтобы правильно выполнилась функция
// преобразования строки в дробное число
string temp = sw.ReadLine().Replace('.', ',');
string[] ver = temp.Split(';');//разбиваем на под массивы
MeshVertex[i].X = Convert.ToSingle(ver[0]);
MeshVertex[i].Y = Convert.ToSingle(ver[1]);
MeshVertex[i].Z = Convert.ToSingle(ver[2]);
}
}
private void LoadFace(int[] Face, int KolString)
{
for (int i = 0; i < KolString; i++)
{
/*Face начинаются с цифры указывающей на тип полигона, но нам нужны только tringles(и в основном только он используются, об
исключениях я к сожалению не знаю).
Пример записи [B]Фэйсов[/B]: 3;0,0,0;,
*/
string temp = sw.ReadLine();
temp = temp.Remove(0, 2);//Так, что отбрасываем первые два символа
temp = temp.Remove(temp.Length - 2);// и последнее точка с запятой (;,) нам не нужны
string[] face = temp.Split(',');
for (int y = 0; y < 3; y++)
{
Face[i * 3 + y] = Convert.ToInt32(face[y]);
}
}
}
private void LoadCoords(float[,] TextureCoords)
{
/*
TextureCoords.Length / 2 - так как у нас двумерный массив и его длина равна столбцы * на строки, поэтому делим на 2
Пример записи [B]текстурных координат[/B]: 0.244999;0.34667;;,
*/
for (int i = 0; i < TextureCoords.Length / 2; i++)
{
string temp = sw.ReadLine().Replace('.', ',');
string[] ver = temp.Split(';');
TextureCoords[i, 0] = Convert.ToSingle(ver[0]);
TextureCoords[i, 1] = Convert.ToSingle(ver[1]);
}
}
private void LoadMaterial(string MaterialStr,bool flag)
{
MaterialName[schetMaterial, 0] = LoadNameMaterial(MaterialStr);
while (true)
{
string tmp = sw.ReadLine();
if (tmp == @"}")
break;
string word = GetFirstWord(tmp);
if (word == @"}")
break;
if ("TextureFilename" == word)
{
MaterialName[schetMaterial, 1] = LoadFileNameTexture(tmp);
if (flag == true)
Meshs[count].fileTexture = @"путь к текстурам" + MaterialName[schetMaterial, 1];
/*
используйте следующие методы получения пути исполняемого файла,
чтобы к ниму прикрепить папку с файлами текстур
System.Reflection.Assembly assem = System.Reflection.Assembly.GetEntryAssembly();
string path = Path.GetDirectoryName(assem.Location);
*/
schetMaterial++;
break;
}
}
}
- теперь напишем функции для отрисовки геометрии; и открытия, привязки текстуры
/*http://esate.ru, Flashhell*/
public void Draw()//вызов дисплейного списка
{
Gl.glCallList(list);
}
public void DrawInMemory()//создание дисплейного списка
{
int nom_l = Gl.glGenLists(1);
// генерируем новый дисплейный список
Vertex temp;
Gl.glNewList(nom_l, Gl.GL_COMPILE);
Gl.glPushMatrix();
for (int i = 0; i < Meshs.Length; i++)
{
Gl.glBegin(Gl.GL_TRIANGLES);
if (Meshs[i].fileTexture != null)
{
loadImage(Meshs[i].fileTexture);
Gl.glEnable(Gl.GL_TEXTURE_2D);
// включаем режим текстурирования , указывая индификатор mGlTextureObject
Gl.glBindTexture(Gl.GL_TEXTURE_2D, mGlTextureObject);
for (int j = 0; j < Meshs[i].MeshFace.Length; j++)
{
int temps = Meshs[i].MeshFace[j];
Gl.glTexCoord2f(Meshs[i].TextCoords[temps, 0], Meshs[i].TextCoords[temps, 1]);
temp = Meshs[i].MeshNormals[Meshs[i].MeshFaceNormals[j]];
Gl.glNormal3f(temp.X, temp.Y, temp.Z);
temp = Meshs[i].MeshVertex[temps];
Gl.glVertex3f(temp.X, temp.Y, temp.Z);
}
}
else
{
for (int j = 0; j < Meshs[i].MeshFace.Length; j++)
{
temp = Meshs[i].MeshNormals[Meshs[i].MeshFaceNormals[j]];
Gl.glNormal3f(temp.X, temp.Y, temp.Z);
temp = Meshs[i].MeshVertex[Meshs[i].MeshFace[j]];
Gl.glVertex3f(temp.X, temp.Y, temp.Z);
}
}
Gl.glEnd();
Gl.glDisable(Gl.GL_TEXTURE_2D);
}
Gl.glPopMatrix();
Gl.glEndList();
list = nom_l;
}
private static uint MakeGlTexture(int Format, IntPtr pixels, int w, int h)
{
// индетефекатор текстурного объекта
uint texObject;
// генерируем текстурный объект
Gl.glGenTextures(1, out texObject);
// устанавливаем режим упаковки пикселей
Gl.glPixelStorei(Gl.GL_UNPACK_ALIGNMENT, 1);
// создаем привязку к только что созданной текстуре
Gl.glBindTexture(Gl.GL_TEXTURE_2D, texObject);
// устанавливаем режим фильтрации и повторения текстуры
Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_WRAP_S, Gl.GL_REPEAT);
Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_WRAP_T, Gl.GL_REPEAT);
Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MAG_FILTER, Gl.GL_LINEAR);
Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MIN_FILTER, Gl.GL_LINEAR);
Gl.glTexEnvf(Gl.GL_TEXTURE_ENV, Gl.GL_TEXTURE_ENV_MODE, Gl.GL_REPLACE);
// создаем RGB или RGBA текстуру
switch (Format)
{
case Gl.GL_RGB:
Gl.glTexImage2D(Gl.GL_TEXTURE_2D, 0, Gl.GL_RGB, w, h, 0, Gl.GL_RGB, Gl.GL_UNSIGNED_BYTE, pixels);
break;
case Gl.GL_RGBA:
Gl.glTexImage2D(Gl.GL_TEXTURE_2D, 0, Gl.GL_RGBA, w, h, 0, Gl.GL_RGBA, Gl.GL_UNSIGNED_BYTE, pixels);
break;
}
// возвращаем индетефекатор текстурного объекта
return texObject;
}
private void loadImage(string imageUrl)
{
Il.ilGenImages(1, out imageId);
// делаем изображение текущим
Il.ilBindImage(imageId);
if (Il.ilLoadImage(imageUrl))
{
// если загрузка прошла успешно
// сохраняем размеры изображения
int width = Il.ilGetInteger(Il.IL_IMAGE_WIDTH);
int height = Il.ilGetInteger(Il.IL_IMAGE_HEIGHT);
// определяем число бит на пиксель
int bitspp = Il.ilGetInteger(Il.IL_IMAGE_BITS_PER_PIXEL);
switch (bitspp) // в зависимости оп полученного результата
{
// создаем текстуру используя режим GL_RGB или GL_RGBA
case 24:
mGlTextureObject = MakeGlTexture(Gl.GL_RGB, Il.ilGetData(), width, height);
break;
case 32:
mGlTextureObject = MakeGlTexture(Gl.GL_RGBA, Il.ilGetData(), width, height);
break;
}
// активируем флаг, сигнализирующий загрузку текстуры
// очищаем память
Il.ilDeleteImages(1, ref imageId);
}
}
Класс готов, для его теста нужна форма с Opengl Control-ом и пару управляющих элементов. Здесь и здесь можно почитать как это сделать. В исходники выложены готовая форма с рабочим классом, и со всем необходим: x-файлами, текстурами. Статья была написана с трудом и без сна , если обнаружите банальные ошибки пожалуйста не обижайтесь.
При загрузке файла из примера-исходника подвигайте ползунки, т.к. размеры модели бывают разные, с ними иногда баг выходит не понятный))), но это не связано с загрузчиком, скорее с контролами.
Надеюсь Вам пригодиться, в будущем планирую написать урок о сохранении Mesh в собственный двоичный формат и загрузки с него. Но это если этот урок хоть кому то пригодиться или следующий станет востребованный.
Понравилась публикация? Сохраните ее, чтобы вернуться к изучению материала!
0
2120
07.01.2011
20
Внимание!
Эта публикация перенесена в раздел уроков по адресу Загрузка .X (DirectX) файлов в OpenGL. К ней прикреплена новая отдельная ветка комментариев форума, которую вы можетет найти после текста публикации.
Обсуждение публикации рекуомендуется вести по новому адресу, который указан выше.
Формат хранения. Все форматы хранят примерно по одинаковому принципу, просто у одних в бинарном формате, у других в текстовом. Все сводится к разбору спецификации и написанию парсера.
А готовые решения (или база от чего оттолкнуться) — это гуд.
В dx есть готовый класс для загрузки. Указал путь к файлу и все дела, загружать даже и не надо в принципе). Любой формат которые удобнее, тот грузим. И принято или не принято, чтобы загружать Mesh в OpenGL надо самому искать решения этой задачи. А можно поинтересоваться, что по Вашему мнению принято использовать с OpenGL?!
Ну, для полноты картины теперь не хватает только загрузчика файлов *.FBX (экспорт по умолчанию из последних 3d studio max). Благо к ним есть целый SDK с примерами, чем не мог похвастаться *.3ds.
Flashhell, я имею ввиду, что microsoft далала этот формат для DirectX. В общем можно использовать формат .x для хранения моделей и в OpenGL. И вообще хорошо, когда ваша программа поддерживает много расширений… Определённого формата для хранения моделей и анимации в GL нет, по этому всё зависит от программиста.
loadImage(string imageUrl)//функция открытия картинки(текстуры) MakeGlTexture(int Format, IntPtr pixels, int w, int h);//привязка текстуры Функции открытия и привязки текстуры с , а загрузчик сам писал, отталкиваясь от примера — . А что?
Можно сделать так: создать свой формат и сделать для него загрузчик, потом сделать конвертер всех современных форматов в свой, тем самым экономим очень и очень много времени и написания лишнего кода (хотя может именно так все и делают =)) )
хай, ну ТЗ нет. просто я зарылся в работе сейчас, а месяц назад, когда Flashhell'a пригласил потестить, я активно каждый день что нить добавлял или правил на новом сайте, и на тот момент конешн в плане тестирования было раздолье. А сейчас так сказать спад моей активности, но можно просто залезть, поюзать что есть, поизучать, обратить внимание на косяки (в основном косяки раздела «е-сеть», там аналог социальной сети, но тоже не доделан еще). Можно создавать группы, темы на форуме и т.д. Полностью другой движок. Но очень много нового функционала еще нет. Ну и много нвого есть =) Можно поделится впечатлениями. Так теперь еще живая лента, чат и т.д. Есть плюшки интересные.
Все сводится к разбору спецификации и написанию парсера.
А готовые решения (или база от чего оттолкнуться) — это гуд.
Любой формат которые удобнее, тот грузим. И принято или не принято, чтобы загружать Mesh в OpenGL надо самому искать решения этой задачи.
А можно поинтересоваться, что по Вашему мнению принято использовать с OpenGL?!
Благо к ним есть целый SDK с примерами, чем не мог похвастаться *.3ds.
MakeGlTexture(int Format, IntPtr pixels, int w, int h);//привязка текстуры
Функции открытия и привязки текстуры с , а загрузчик сам писал, отталкиваясь от примера — .
А что?
Можно создавать группы, темы на форуме и т.д. Полностью другой движок.
Но очень много нового функционала еще нет. Ну и много нвого есть =) Можно поделится впечатлениями.
Так теперь еще живая лента, чат и т.д. Есть плюшки интересные.
Буду ковырять на выходных и когда на работе скучно становится