Загрузка .X (DirectX) файлов в OpenGL
Что прочитать перед этим материалом
Загрузка файлов трехмерных моделей в формате .X (DirectX)
Так как OpenGL - это кросс-платформенная библиотека, которая была написана для вывода графики, а не для работы с файловой системой и потоками, поэтому в ней нет встроенных решений для загрузки трехмерных объектов (Meshs) из файла.
В это уроке будет подробно рассмотрен несложный способ загрузки файла в формате .X (DirectX). Для этого будет использована связка C# + Tao.Framework, поэтому желательно почитать пару уроков, как ее использовать.
Обратите внимание!
После установки Tao все библиотеки из C:\Program Files\TaoFramework\lib нужно скопировать в C:\Windows\System32, иначе библиотек не будет в системе, а Visual Studio не будет их видеть.
.X файл - формат файла для хранения 3D-объектов (Meshs). Файл может быть записан как текстовый - “.X” или бинарный - “.bin”.
Формат X хранит информацию 3D-объекта:
- координаты вершин (Vertex), последовательность их загрузки (Face);
- координаты нормалейv(Vertex), последовательность их загрузки (Face);
- текстурные координаты, описание материалов и названия текстур.
Vertex - это точка в трехмерном пространстве, содержащая в себе координаты X Y Z.
Normals - это вектор направленный вверх от полигона, предназначенный для расчета света.
Полигон - это простая фигура сложенная в трехмерном пространстве из Vertex-ов.(например: треугольник, прямоугольник).
Вот выглядит простой куб, экспортированный в файл формата .X с помощью CyberMotion 3D-Designer (v12.0):
Код:
xof 0302txt 0064
Material NameMaterial { // Material - ключевое слово,
1.0;1.0;1.0;1.000000;; // NameMaterial - имя материала
0.000000;
1.0;1.0;1.0;;
0.000000;0.000000;0.000000;;
}
Material Basic {
1.0;1.0;1.0;1.000000;;
0.000000;
1.0;1.0;1.0;;
0.000000;0.000000;0.000000;;
}
Mesh Box { // Mesh - ключевое слово; Box - имя 3D-объекта (Meshs)
8; // количество Vertex-ов
-33.5;32.5;-33.5;, // сами Vertex-ы
32.5;32.5;-33.5;,
32.5;-33.5;-33.5;,
-33.5;-33.5;-33.5;,
-33.5;32.5;32.5;,
32.5;32.5;32.5;,
32.5;-33.5;32.5;,
-33.5;-33.5;32.5;;
// Mesh Face
12; // количество Face-ов
3;1,2,0;, // указывает какой тип полигона
3;2,3,0;, // используется (3 triangles, 4 quads...) 1,2,0;, = Face-ы
3;4,6,5;,
3;4,7,6;,
3;0,4,1;,
3;4,5,1;,
3;1,5,2;,
3;5,6,2;,
3;2,6,3;,
3;6,7,3;,
3;3,7,0;,
3;7,4,0;;
MeshMaterialList {
0;
12;
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0;;
}
MeshNormals {
12; // количество нормалей Vertex-ы
0.0;0.0;-1.0;, // Vertex-ы нормалей
0.0;0.0;-1.0;,
0.0;0.0;1.0;,
0.0;0.0;1.0;,
0.0;1.0;0.0;,
0.0;1.0;0.0;,
1.0;0.0;0.0;,
1.0;0.0;0.0;,
0.0;-1.0;0.0;,
0.0;-1.0;0.0;,
-1.0;0.0;0.0;,
-1.0;0.0;0.0;;
12; // количество Фэйсов (Face)
3;0,0,0;, // Фэйсы (Face)
3;1,1,1;,
3;2,2,2;,
3;3,3,3;,
3;4,4,4;,
3;5,5,5;,
3;6,6,6;,
3;7,7,7;,
3;8,8,8;,
3;9,9,9;,
3;10,10,10;,
3;11,11,11;;
}
}
А вот выглядит простой куб, но уже текстурированный (с материалами) в .X-файле, экспортированный с помощью CyberMotion 3D-Designer (v12.0):
Код:
xof 0302txt 0064
Material Material_2 { // Material - это ключевое слово, а Material_2 это его имя
1.0;1.0;1.0;1.000000;;
0.000000;
1.0;1.0;1.0;;
0.852088;0.852088;0.852088;;
TextureFilename {"cube_uv.jpg";} // имя текстуры
}
Material Basic {
1.0;1.0;1.0;1.000000;;
0.000000;
1.0;1.0;1.0;;
0.000000;0.000000;0.000000;;
}
Mesh Box_2 {
24;
-50.0;13.895662;-50.0;,
50.0;13.895662;-50.0;,
50.0;-87.104338;-50.0;,
-50.0;-87.104338;-50.0;,
-50.0;13.895662;50.0;,
50.0;13.895662;50.0;,
50.0;-87.104338;50.0;,
-50.0;-87.104338;50.0;,
50.0;13.895662;-50.0;,
50.0;-87.104338;-50.0;,
-50.0;13.895662;-50.0;,
-50.0;-87.104338;-50.0;,
-50.0;13.895662;50.0;,
50.0;-87.104338;50.0;,
50.0;13.895662;50.0;,
-50.0;-87.104338;50.0;,
-50.0;13.895662;-50.0;,
-50.0;13.895662;50.0;,
50.0;13.895662;-50.0;,
50.0;13.895662;50.0;,
50.0;-87.104338;-50.0;,
50.0;-87.104338;50.0;,
-50.0;-87.104338;-50.0;,
-50.0;-87.104338;50.0;;
12;
3;8,9,10;,
3;9,11,10;,
3;12,13,14;,
3;12,15,13;,
3;16,17,18;,
3;17,19,18;,
3;1,5,20;,
3;5,21,20;,
3;2,6,22;,
3;6,23,22;,
3;3,7,0;,
3;7,4,0;;
MeshMaterialList {
1;
12;
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0;;
{Material_2} // указание на использованный материал, по его имени
}
MeshNormals {
12;
0.0;0.0;-1.0;,
0.0;0.0;-1.0;,
0.0;0.0;1.0;,
0.0;0.0;1.0;,
0.0;1.0;0.0;,
0.0;1.0;0.0;,
1.0;0.0;0.0;,
1.0;0.0;0.0;,
0.0;-1.0;0.0;,
0.0;-1.0;0.0;,
-1.0;0.0;0.0;,
-1.0;0.0;0.0;;
12;
3;0,0,0;,
3;1,1,1;,
3;2,2,2;,
3;3,3,3;,
3;4,4,4;,
3;5,5,5;,
3;6,6,6;,
3;7,7,7;,
3;8,8,8;,
3;9,9,9;,
3;10,10,10;,
3;11,11,11;;
}
MeshTextureCoords { // текстурные координаты
24;
0.244999;0.34667;
0.510002;0.34667;
0.489998;0.673335;
0.244999;0.65333;
0.020005;0.34667;
0.734997;0.34667;
0.489998;0.979995;
0.020005;0.65333;
0.489998;0.34667;
0.489998;0.65333;
0.265003;0.34667;
0.265003;0.65333;
0.979995;0.34667;
0.755001;0.65333;
0.755001;0.34667;
0.979995;0.65333;
0.265003;0.326665;
0.265003;0.020005;
0.489998;0.326665;
0.489998;0.020005;
0.510002;0.65333;
0.734997;0.65333;
0.265003;0.673335;
0.265003;0.979995;;
}
}
Рассмотрев структуру файла внимательней, можно понять, какие слова являются ключевыми и как разграничиваются записи данных.
Загрузчик был специально создан под программу CyberMotion 3D-Designer, но CyberMotion 3D-Designer не совсем удобен, так как для его использования необходима лицензия.
Поэтому загрузчик был модифицирован для большей универсальности. Так что он сможет загружать .Х-файлы, экспортированные через Blender.
Blender - бесплатная программа, которую легко найти и скачать, кроме того, она работает с многими известными форматами.
Единственное, что может показаться недостатком, так это то, что придется установить Python (точнее его интерпретатор).
Внимание! Данный загрузчик - пробный и не исключены ошибки и малопроизводительные решения.
Код загрузчика
Сначала создаем проект оконного приложения С#.
Добавим в References (ссылки) Tao.FreeGlut, Tao.DevIl, Tao.OpenGl, Tao.Platform.Windows.

Подключим библиотеки (директива using):
Код:
using System.IO;
using Tao.FreeGlut;
using Tao.OpenGl;
using Tao.Platform.Windows;
using Tao.DevIl;

Прописываем создание класса в Form1 (или как Вы его назвали) .cs:
Код:
public partial class Form1 : Form
{}
public class mesh
{}
Добавим глобальные переменные:
Код:
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, или написать свою. Какую использовать, решать Вам:
Код:
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]; / /возвращаем первый элемент массива, то есть первое слово
}
Теперь, когда есть базовая функция, можем приступить к функции определения ключевых слов:
Код:
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;
}
Напишем функцию открытия файла:
Код:
public void Open(string url)
{
sw = new StreamReader(url, Encoding.Default); // открываем поток чтения файла
string temp;
while ((temp = sw.ReadLine()) != null) // считаем количество объектов и материалов для выделения на них памяти
{
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); // снова открываем уже прочтенный поток
}
Создаем конструкторы класса:
Код:
public mesh()
{
}
public mesh(string url) // основной конструктор для открытия и чтения
{
Open(url);
while (Read() == true) // читаем по строке пока поток не кончиться
{
}
DrawInMemory();
}
Теперь создадим отдельные функции для загрузки определенных ключевыми словами (Mesh, MeshNormals, Material, MeshTextureCoords) данных и вложенных в них данных (Vertex, Face), но сначала создадим структуры, куда будем это все записывать:
Код:
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
}
Функции:
Код:
private string LoadFileNameTexture(string StrTextureFilename) // функция загрузки имени файла текстуры
{
string[] temp = StrTextureFilename.Split('\"'); // имя файла взято в кавычки, поэтому делим на массивы отталкиваясь от этого
return temp[1]; // возвращаем второй элемент массива, то есть то, что находится за первой кавычкой и до последней
}
private string LoadNameMaterial(string str) // функция загрузки имени Material-а
{
int start = 0; // тот же принцип, что и в GetFirstWord
while (str[start] == ' ')
{
start++;
}
if (start > 0)
{
str = str.Substring(start, (str.Length - start));
}
string[] temp = str.Split(' ');
return temp[1]; // только возращаем второе слово
}
private void LoadMesh() // функция загрузки Mesh-а
{
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;
}
}
}
Теперь напишем функции для отрисовки геометрии, а также открытия и привязки текстуры:
Код:
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 и паруой управляющих элементов.