Формат 3ds описание. Чем открыть файл.3DS

Вайбер на компьютер 31.03.2019
Вайбер на компьютер

Структура формата.3DS

Всем привет, меня зовут Осипов Дмитрий (AMM1AK), и я хотел бы написать подробную статью о структуре.3DS формата. Не кричите сильно и не кидайте тапками, если не понравится. Просто это моя первая статья J.
Теперь хочу сказать пару слов о том, почему бы я хотел написать именно об этом.
Вообще я занимаюсь программированием 3D графики (Delphi + OpenGL). Раньше для экспорта моделей я использовал 3D Studio Max и писал небольшой экспортер на Max Script’е. Но написать хорошую утилиту, которая бы могла сохранять модель любой сложности, плюс к этому сохранять несколько материалов и помимо текстуры сохранять еще и Bump и Alpha текстуры мне так и не удалось. Хороших учебников на русском я не смог найти, была какая-то книга, но про работу с файлами там ничего не было. Вот таким образом через какое-то время, когда я переписывал свой движок с нуля, я и решил написать небольшую утилиту на Delphi, которая бы могла просматривать модели.3DS формата и плюс к этому сохранять в свой. Но опять появились проблемы. Я перерыл весь Интернет, но не смог найти полной документации о.3DS формате. Конечно, попадались небольшие статьи об этом, но там было все поверхностно, потому и приходилось сидеть часами и угадывать байты J. Вот таким образом я сейчас сижу и пишу статью для тех, кто тоже столкнулся с моей проблемой.
Ну что же? Давайте перейдем к делу. Вообще.3DS это очень сложный формат, который помимо самой геометрии включает в себя всю информацию о материалах, о настройках камеры, о свете, о настройках окон редактора и.т.д. Поэтому для того, чтобы можно было его корректно прочитать, формат выстроен в виде дерева, его ветви это блоки, у каждого блока есть родительский блок (за исключением самого главного) и у некоторых блоков есть дочерние блоки. Теперь давайте расскажу вообще о блоках. Каждый блок состоит из трех частей. Первая часть это его идентификатор, она занимает 2 байта, вторая это длина в байтах всего блока, включая первый и второй блоки, эта часть занимает 4 байта, ну а третья часть это сами данные, она занимает столько, сколько во втором блоке минус шесть (то есть минус длину первого и второго блоков). Тут у разных блоков по-разному, у одних тут какие-то данные, у других дочерние блоки, а у некоторых вообще пусто. Ниже я привел полный, на мой взгляд, список блоков с их идентификаторами. Почему “на мой взгляд”, потому что этот список я составлял сам, складывая блоки увиденные мной в разных демо с исходным кодом, и используя те статьи, что были. Конечно это не полный список, сам проверял, но тут намного больше того, что нужно для полноценного экспорта статичных моделей.

MAIN_CHUNK = $4D4D;
VERSION = $0002;
EDIT3DS = $3D3D;
EDIT_MATERIAL = $AFFF;
MAT_NAME = $A000;
MAT_AMBIENT = $A010;
MAT_DIFFUSE = $A020;
MAT_SPECULAR = $A030;
MAT_SHININESS = $A040;
MAT_SHIN2PCT = $A041;
MAT_SHIN3PCT = $A042;
MAT_TRANSPARENCY = $A050;
MAT_XPFALL = $A052;
MAT_TWO_SIDED = $A081;
MAT_SELF_ILPCT = $A084;
MAT_WIRE = $A085;
MAT_WIRESIZE = $A087;
MAT_XPFALLIN = $A08A;
MAT_SHADING = $A100;
MAT_REFL_BLUR = $A053;
MAT_TEXTURE_MAP = $A200;
MAT_REFLECTION_MAP = $A220;
MAT_BUMP_MAP = $A230;
EDIT_OBJECT = $4000;
EDIT_OBJ_TRIMESH = $4100;
TRI_VERTEX_LIST = $4110;
TRI_FACE_LIST = $4120;
TRI_MESH_MTRGROUP = $4130;
TRI_TEXVERTS = $4140;
TRI_SMOOTHGROUPS = $4150;
TRI_LOCAL_AXES = $4160;
EDIT_CONFIG = $3E3D;
EDIT_CONFIG1 = $0100;
EDIT_VIEWPORT1 = $7012;
KEYF3DS = $B000;

Внимательнее следите за отступами, что бы понять иерархию дерева. Самый главный блок, как видите, это MAIN_CHUNK, его ID - 4D4D в шестнадцатеричной системе. Если мы посмотрим его длину, то получим длину всего файла. Так таковых данных у него нет, но он включает в себя дочерние блоки, такие как VERSION, EDIT3DS, EDIT_CONFIG, EDIT_CONFIG1, EDIT_VIEWPORT1 и KEYF3DS. Теперь вкратце о каждом из них. Блок VERSION содержит 4 байта данных, что в них, я думаю, вы уже догадались. Блок EDIT3DS содержит 10 байт данных, я не знаю что в них и два набора блоков, таких как EDIT_MATERIAL (их столько, сколько материалов на сцене) и EDIT_OBJECT (их столько, сколько мешей на сцене). Блоки EDIT_CONFIG, EDIT_CONFIG1, EDIT_VIEWPORT1 содержат информацию о настройке окон вида и они нам не понадобятся, поэтому их пропускаем. Последний блок KEYF3DS содержит информацию об анимации сцены, для экспорта статичных моделей он тоже не нужен. Теперь давайте разберем блоки EDIT_MATERIAL и EDIT_OBJECT. Как я уже писал выше, блоков EDIT_MATERIAL будет столько, сколько материалов на сцене. Внутри этого блока содержится большое количество дочерних блоков, большая их часть нам даже не понадобятся. Блок EDIT_OBJECT содержит один дочерний блок EDIT_OBJ_TRIMESH, который в свою очередь содержит блоки с информацией меша.
Как видите этот формат очень большой, и именно для нас тут хранится очень много не нужного. Вот именно поэтому я и решил создавать свой формат, а не использовать.3DS. Ниже я привел список блоков, в котором я оставил только самое необходимое для нас.

MAIN_CHUNK = $4D4D;
VERSION = $0002;
EDIT3DS = $3D3D;
EDIT_MATERIAL = $AFFF;
MAT_NAME = $A000;
MAT_AMBIENT = $A010;
MAT_DIFFUSE = $A020;
MAT_SPECULAR = $A030;
MAT_TEXTURE_MAP = $A200;
MAT_TRANSPARENCY_MAP = $A210;
MAT_BUMP_MAP = $A230;
EDIT_OBJECT = $4000;
EDIT_OBJ_TRIMESH = $4100;
TRI_VERTEX_LIST = $4110;
TRI_FACE_LIST = $4120;
TRI_MESH_MTRGROUP = $4130;
TRI_TEXVERTS = $4140;

В разделе EDIT_MATERIAL я оставил лишь блок с именем материала (MAT_NAME). Блоки с рассеянным, диффузным и зеркальным цветами (MAT_AMBIENT, MAT_DIFFUSE, MAT _SPECULAR). А также три блока, которые хранят данные о текстуре материала, текстуре прозрачности и текстуре нормалей (MAT_TEXTURE_MAP, MAT_TRANSPARENCY_MAP, MAT_BUMP_MAP) соответственно. В блоке EDIT_OBJ_TRIMESH остались следующие блоки. Это блок координат точек(TRI_VERTEX_LIST), блок с информацией о гранях(TRI_FACE_LIST), блок с информацией о материалах меша (TRI_MESH_MTRGROUP) и, наконец, блок с информацией о текстурных координатах (TRI_TEXVERTS).
Теперь предлагаю разработать наши собственные типы данных для хранения информации о модели. Ниже приведен исходный код всех типов необходимых для работы.
type
TVertex3f = record // Пространственная точка
X, Y, Z: GLFloat;
end;
TVertex2f = record // Точка на плоскости
X, Y: GLFloat;
end;
TColor = array of GLFloat; // Цвет (R, G, B)
TFace = record // Грань
Verts: array of Word; // Индексы точек
Mat: Word; // Номер материала из массива маиериалов
end;
TModel = record // Модель (тут думаю все понятно)
Name: String; // Ее имя
VertexsCount, // Количество точек
FacesCount, // Количество треугольников
TexVertexsCount: Word; // Количество текстурных точек
Vertexs: array of TVertex3f; // Массив точек
Faces: array of TFace; // Массив треугольников
TexVertexs: array of TVertex2f; // Массив текстурных точек
end;
TMaterial = record // Материал
Name: String; // Его имя
AmbientColor, // Рассеянный цвет
DiffuseColor, // Диффузный цвет
SpecularColor: TColor; // Зеркальный цвет
TextureMap, // Путь к текстуре
AlphaMap, // Путь к альфа текстуре
NormalMap: String; // Путь к текстуре нормалей
end;
T3DSModel = object // 3DS сцена
ModelsCount, // Количество моделей
MaterialsCount: Word; // Количество материалов
Models: array of TModel; // Массив моделей
Materials: array of TMaterial; // Массив материалов
TempIndexModel,
TempIndexMaterial: Integer; // Две переменные нужные для работы
Procedure Clear; // Процедура очистки модели
Procedure AddModel; // Процедура выделения памяти под очередную модель
Procedure AddMaterial; // Процедура выделения памяти под очередной материал
Procedure Laod(FileName: String); // Процедура загрузки
Procedure Draw; // Процедура отрисовки
end;

Ну вот, это в принципе все что нужно. В типе TFace массив из трех элементов, это индексы точек, которые сами находятся в массиве, а Mat это индекс материала, который находится в массиве материалов в типе T3DSModel. Также в тип T3DSModel я записал две переменные TempIndexModel и TempIndexMaterial, это что-то вроде индекса текущей модели и текущего материала. Теперь предлагаю написать процедуры типа T3DSModel, пока только до загрузки.

Procedure T3DSModel.Clear;
begin
MaterialsCount:= 0; // Ставим количество материалов
ModelsCount:= 0; // Ставим количество моделей
SetLength(Models, 0); // Устанавливаем длину массива моделей в 0 элементов
SetLength(Materials, 0); // Устанавливаем длину массива материалов в 0 элементов
TempIndexModel:= -1;
TempIndexMaterial:= -1; // Ставим индексы в -1
end;

Вот, это была процедура очистки модели, её можно всунуть куда-нибудь на событие кнопки Reset, как в 3DSMax’е или перед открытием другой модели. Дале пишу код процедур добавления модели и материала.

Procedure T3DSFile.AddModel;
begin
inc(ModelsCount); // Увеличиваем количество моделей на 1
SetLength(Models, ModelsCount); // Устанавливаем новую длину массива моделей
inc(TempIndexModel); // Увеличиваем временной индекс на 1
end;

Procedure T3DSFile.AddMaterial;
begin
inc(MaterialsCount); // Увеличиваем количество материалов на 1
SetLength(Materials, MaterialsCount); // Устанавливаем новую длину массива материалов
inc(TempIndexMaterial); // Увеличиваем временной индекс на 1
end;

Ну вот, тут тоже ничего сложного. Сначала увеличиваем количество, потом устанавливаем длину массива по только что увеличенной переменной, и увеличиваем временной индекс. Заметьте, что временной индекс всегда равен количеству моделей или материалов минус один, т.к. если у нас пять моделей в массиве, а массивы как нам известно начинаются с 0, то последний индекс это четыре. Вот с элементом массива, индекс которого равен TempIndexModel мы и будем производить какие-либо действия.
Вот выкладываю код еще двух процедур необходимых для работы. Первая процедура считывает строку из файла, а вторая цвет, определяя при этом как он зашифрован (его составляющие от 0 до 1 или от 0 до 255). Еще две константы, нужные для работы второй процедуры, которые можно добавить в общий список COLOR_FLOAT = $0010; COLOR_UBYTE = $0011;

Function ReadString(var F: TFileStream): string;
var
lchar: char;
res: string;
begin
res:="";
f.Read(lchar, 1);
while (lchar <> #0) do
begin
res:=res + lchar;
f.Read(lchar, 1);
end;
Result:=res;
end;

Function ReadColor(var f: TFileStream): TColor;
var Rf, Gf, Bf: GLfloat;
Rub, Gub, Bub: GLubyte;
chunk_id: GLushort;
chunk_length: GLuint;
begin
Rub:=0; Gub:=0; Bub:=0;
f.Read(chunk_id, 2);
f.Read(chunk_length, 4);
case chunk_id of
COLOR_FLOAT:
begin
f.Read(Rf, 4);
f.Read(Gf, 4);
f.Read(Bf, 4);
end;
COLOR_UBYTE:
begin
f.Read(Rub, 1);
f.Read(Gub, 1);
f.Read(Bub, 1);
Rf:=Rub/255;
Gf:=Gub/255;
Bf:=Bub/255;
end;
else f.Seek(chunk_length-6, soFromCurrent);
end;
Result:=Rf;
Result:=Gf;
Result:=Bf;
end;

Теперь предлагаю перейти к самому главному в этой статье, это загрузке сцены из.3DS формата. Ниже я приведу исходный код с комменьариями.

Procedure T3DSFile.Load(FileName: String);
var
ChunkID, usTemp: Word; // Тот самый идентификатор блоков (ID) и одна вспомогательная переменная
ChunkLen: Cardinal; // Переменная для записи в неё длины блоков
f: TFileStream; // файл
i: Integer; // просто I J
Patch: String; // Строка
//==================================
FormatVesion: Cardinal; // Версия файла, может кому пригодится, мне нет, но все равно считываю J
MatNum: Word;
TriangleNum: Word; // Тоже две вспомогательные переменные
begin
f:= TFileStream.Create(FileName, fmOpenRead); // Открываем файл, заметьте что при запуске процедуры мы получаем путь к файлу
f.Read(ChunkID, 2); // Считываем ID первого блока, это как вы помните MAIN_CHUNK
f.Read(ChunkLen, 4); // Считываем его длину
if ChunkID <> MAIN_CHUNK then {Если только что прочитанный ID не равен MAIN_CHUNK, то это не.3DS файл. Нужно вывести сообщение и выйти из процедуры}
begin
MessageBox(0, PChar("Файл: "+FileName+" поврежден"), "Ошибка!", 0);
Exit;
end;
while f.Position < f.Size do // Цикл. Пока позиция чтения меньше размера всего файла, делаем …
begin
f.Read(ChunkID, 2); // Снова считываем ID
f.Read(ChunkLen, 4); // Считываем длину
case ChunkID of // Далее идет оператор case. Ищем совпадения считанного ID с известными нам константами.
EDIT3DS: f.Seek(10, soFromCurrent); // Если это EDIT3DS, то пропускаем 10 байт. Выше я писал про это.
VERSION: f.Read(FormatVersion, 4); // Если это VERSION, то считываем 4 байта в переменную, потом юзайте её как душе угодно
EDIT_OBJECT: // Если это блок объектов, то …
begin
AddModel; // Та самая процедура выделения памяти еще под одну модель.
Models.Name:= ReadString(f); {Тут же считываем имя модели. Заметьте, вот тут мы начинаем пользоваться теми самыми временными индексам}.
end;
EDIT_OBJ_TRIMESH: ; {если это EDIT_OBJ_TRIMESH, то ничего не делаем. Прошу обратить внимание, что мы не пропускаем байты этого блока, как в EDIT3DS, так как длина данного блока включает длину дочерних блоков, а они нам нужны}
TRI_VERTEX_LIST: // Если это TRI_VERTEX_LIST, то …
begin
f.Read(Models.VertexsCount, SizeOf(Word)); // Считываем количество точек текущего объекта
SetLength(Models.Vertexs, Models.VertexsCount); // Устанавливаем длину массива
for i:= 0 to Models.VertexsCount - 1 do // Запускаем цикл …
begin
f.Read(Models.Vertexs.X, SizeOf(Single)); // Считываем х координату итой точки, далее z, далее у, заметьте z и у поменяны
.Z, SizeOf(Single)); // местами, так как в 3DSMax’е перепутаны оси J
f.Read(Models.Vertexs
.Y, SizeOf(Single));
end;
end;
TRI_FACE_LIST: // Если это TRI_FACE_LIST, то …
begin
f.Read(Models.FacesCount, SizeOf(Word)); // Считываем количество треугольников
SetLength(Models.Faces, Models.FacesCount); // Настраиваем длину массива
for i:= 0 to Models.FacesCount - 1 do
begin
.Verts, SizeOf(Word));
f.Read(Models.Faces
.Verts, SizeOf(Word));
f.Read(Models.Faces
.Verts, SizeOf(Word)); // Считываем индексы вершин данного треугольника
f.Read(usTemp, SizeOf(Word)); // Еще выделены два байта под флаги треугольника, за что они отвечают я не знаю
end;
end;
TRI_TEXVERTS: // Если это TRI_TEXVERTS
begin
f.Read(Models.TexVertexsCount, SizeOf(Word)); // Считываем количество текстурных точек
SetLength(Models.TexVertexs, Models.TexVertexsCount); // Настраиваем массивы
for i:= 0 to Models.TexVertexsCount - 1 do
begin
.X, SizeOf(Single));
f.Read(Models.TexVertexs
.Y, SizeOf(Single)); // Циклом считываем Х и У координаты
end;
end;
EDIT_MATERIAL: // Если это EDIT_MATERIAL
begin
AddMaterial; // Добавляем материал в массив
Materials.Clear; // Очищаем добавленный материал (на всякий случай J)
end;
MAT_NAME: // Если это MAT_NAME
begin
Materials.Name:= ReadString(f); // Считываем строку с именем материала
end;
MAT_AMBIENT: // Если это MAT_AMBIENT
begin
Materials.Ambient:= ReadColor(f); // Считываем цвет
end;
MAT_DIFFUSE: // По аналогии
begin
Materials.Diffuse:= ReadColor(f);
end;
MAT_SPECULAR: // По аналогии
begin
Materials.Specular:= ReadColor(f);
end;
MAT_TEXTURE_MAP: // Если это MAT_TEXTURE_MAP, то … Последние три блока, это пожалуй самое сложное J
begin
f.Read(ChunkID, 2); // Тут должны быть какие-то вложенные
f.Read(ChunkLen, 4); // блоки, но про них ничего не известно,
f.Read(ChunkID, 2); // поэтому просто пропускаем байты
f.Read(ChunkLen, 4);
f.Read(usTemp, 2);
Materials}

Рекомендуем почитать

Наверх