урок 12
Программа на данном этапе умеет отрисовывать 3D-модели, хранящиеся непосредственно в коде программы - т.н. "захардкоженные". Чтобы изменить или подредактировать отображаемый объект, нужно непосредственно в файле "main.c" подредактировать точки и треугольники модели, сохранить файл - и перекомпилировать программу. Подход с перекомпиляцией программы при изменении модельки - хоть и имеет право на жизнь (и даже, возможно, имеет свои плюсы - в частности уменьшение времени загрузки игры), но несёт с собой столько минусов - неудобств и ограничений, что практически не применяется уже лет 30-40 как.
Процесс создания игры удобно максимально разнести с процессом создания контента - художникам и всяким прочим art'истам должны быть предоставлены редакторы, в которых они, нажимая кнопочки на экране могут загружать свои наработки в игру и творить, не отвлекаясь на отвлечение программистов от сотворения движка. Для возможности загрузки файлов двухмерных и трёхмерных редакторов (и каких-нибудь скриптов, и проч., но это за скобками пока), нужно разработать отдельную цепочку программ для конвертации информации из этих программ или же обеспечить возможность их загрузки непосредственно в игру. В любом случае, не обойтись без файловых операций - как минимум чтение файла и, возможно, запись в файл, а так же здесь напрашивается работа с динамической памятью - чтобы было куда в нашей программе информацию из этих файлов сохранять. Вообще-то серьёзные игровые движки реализуют собственные механизмы работы с памятью, так как память - одно из главных "бутылочных горлышек" на пути в борьбе за производительность (и изобретательность созателей игр в этой борьбе, порой, поражает!). Пока мы не настолько притязательны в этом плане, вполне можно позволить себе использовать и стандартные, предоставляемые языком (точнее не языком, а библиотекой) средства работы с памятью.
В нулевом уроке написание проекта началось с элементарной программы "Helloworld" - сначала с использованием библиотечной функции printf()
, а затем простого её заменителя на ассемблере. Код, написанный нами на ассемблере, непереносим - т.е. если мы захотим, чтобы программа, использующая наш ассемблерный псевдоаналог printf()
(на самом деле puts()
), заработала на другой операционной системе, или на другой архитектуре процессора - нужно соответственно переписать ассемблерный код. Функции стандартной библиотеки же как раз и унифицируют всё взаимодействие с операционками и архитектурами, оставляя пользователю (пользователь стандартной библиотеки - это программист, естественно) единый стандартный интерфейс, то есть одинаковые для любой системы функции, такие как раз, как printf()
. Стандартная библиотека настолько стандартна (в бытовом смысле слова), что многие программисты считают её частью языка. Но стандартна она не только тем, что она есть практически для любой системы, но и в предоставляемых интерфейсах. Так вывод на экран, которым мы пользовались до сего момента (и будем продолжать пользоваться и далее, чего уж там) - является, по факту, выводом на стандартный вывод, предоставленный операционной системой. Операционная система - скорее всего - стандартным выводом предоставит программе виртуальную консоль, или терминал, но вполне может предоставить и что-то другое - например принтер, файл или, к примеру, какой-нибудь из физических портов. printf()
же со всем этим разнообразием будет работать одинаково - что с файлом, что с терминалом. Для printf()
всё это разнообразие - всего лишь поток, в который она отдаёт символы, один за другим.
Если запустить программу "как обычно", введя имя программы в командной строке - операционная система подготавливает к работе программу и, "по умолчанию", открывает для неё три стандартных потока - стандартный поток ввода, стандартный поток вывода, стандартный поток ошибок. Все эти три потока связываются с терминалом, из которого программа запущена. Таким образом при выводе программы текста на стандартный вывод мы видим этот текст в терминале.

Итак, функция printf()
используется для (форматированного) вывода на стандартный поток вывода - чем бы этот поток не являлся. Но это частный случай. Для (форматированного) вывода в общем случае, в какой-нибудь конкретный поток, выбранный самой программой, предназназначена функция fprintf()
. Основное отличие от printf()
в том, что fprintf()
принимает ещё и идентификатор потока, в который нужно осуществить вывод. Чаще всего в качестве потока используется файл, но это не обязательно. Например, вызовы printf("test texst\n")
и fprintf(stdout, "test text\n")
практически аналогичны, хотя библиотечная реализация и может вносить свои незначительные различия между эффектами этих двух функций. Первым аргументом функции fprintf()
указывается указатель на переменную типа FILE
- как сам тип FILE
, так и указатель на структуры этого типа stdout
описаны в файле "stdio.h".
Если мы хотим вместо заранее готовых стандартных потоков ввода-вывода использовать наши собственные потоки, необходимо, во-первых, объявить переменную типа указатель на FILE
, и, во-вторых, заполнить эту структуру (тип FILE
- это тип структура, содержащий необходимую библиотечным функциям типа fprintf()
информацию о файле, позволяющую работать с ним как с потоком данных). После этого можно вызывать функцию fprintf()
, передав в неё указатель на файл. Структура FILE
хоть и может быть проанализирована и заполнена вручную, делать этого не следует: структура является внутренней библиотечной структурой, и она может отличаться не только между различными ОС, но и от версии к версии библиотеки. Программист не должен работать непосредственно с полями библиотечных структур, а использовать для их заполнения предоставляемые библиотекой же функции. Так, функция fopen()
откроет (если это возможно) файл, и вернёт указатель на соответствующую этому файлу (корректно заполненную) структуру, соответствующую этому файлу - или вернёт указатель на NULL, если по какой-то причине открыть файл не удалось. Функция fopen()
принимает два аргумента - имя файла, который надо открыть, и режим, в котором файл требуется открыть - например нам нужен режим записи, "w", также файл можно открыть для чтения - "r", добавления данных (записи в конец) - "a", а некоторые системы позволяют открыть его ещё и в двоичном режиме - "b".

Вышеприведённый пример работает, но файловые операции - очень подвержены ошибкам. Файл может не открыться по множеству причин - при создании файла такой файл уже может существовать, или может не быть места на диске, может не существовать указанного пути. При чтении - файла может просто не быть. В любом случае у программы может не хватить прав доступа. Поэтому при работе с файлами всегда нужно проверять, получилось ли открыть файл - попытка записи в неоткрытый поток приведёт к аварийному завершению программы. Иногда это и может быть оправдано - не получилось, значит всё. Но в подавляющем большинстве случаев при невозможности файловой операции стоит лишь известить пользователя (программы) о неудаче, и предоставить ему выбор дальнейших действий. И, конечно же, как и всегда, нужно освобождать ресурсы. В нашем простейшем случае операционная система сама закроет файл при завершении программы, но в любой хоть сколько нибудь более сложной программе, чем предыдущий игрушечный пример, пренебрегать закрытием ресурсов не стоит хотя бы из соображений наработки хороших привычек программирования.

fopen()
вернёт указатель на NULL, и, соответственно, программа известит о ошибке)
(Если, из соображений безопасности, в вашей системе отсутствуют потенциально опасные утилиты su, sudo и подобные, способные повышать полномочия (чем, естественно, нивелируют всю суть разграничения прав доступа) - под другим пользователем можно работать, залогинившись в соседнем виртуальном терминале - Ctrl+Shft+F2)
Теперь встаёт следующая дилемма: работать с файлами мы научились "текстовым" образом, как с потоком символов - набором привычных для человеческого восприятия букв, цифр и знаков препинания. В написанной же ранее программе самолётик, который мы учились отрисовывать в прошлых уроках - представлен в памяти компьютера в виде набора чисел с плавающей точкой, т.е. зависящий от самой программы набор бит, бинарных цифр. Чтобы научить программу отрисовывать загруженную из файла 3D-модель, нужно либо научиться в момент чтения файла преобразовывать закодированную в тексте модель в набор чисел с плавающей точкой в виде, понятном программе, либо изначально создавать файлы с моделью в бинарном представлении (где содержимое файла будет непосредственно предоставлять содержимое переменной в программе, хранящей модель - тогда нужно научиться работать с файлами и в бинарном режиме. В серьёзной коммерческой игре лучше (в интересах оптимизации) использовать второй подход - бинарные файлы в общем случае меньше весят и при загрузке не нужно будет тратить время на конвертацию к внутреннему представлению программы. Но, так как в любом случае нужно будет написать конвертер из файла, предоставляемого программой 3D-моделирования в формат программы (либо написать модуль сохранения для 3D редактора, ну или сам 3D-редактор) - можно просто найти такой формат 3D файлов, в котором модели хранятся в текстовом виде - так, что при в его структуре возможно разобраться даже без спецификации, просто открыв файл текстовым редактором. И, не гонясь пока особо за эффективностью, модели в игру грузить можно непосредственно из этого файла.
Создадим следующий файл (в нём будет описана та же модель, что и была в коде программы, в том же виде - но сам файл будет текстовым): "plane_model.hrz" - сохранить его следует, согласно решению о организации файлов в структуре проекта, в директории "res/models"
Теперь можно отрисовывать ту же модель самолёта, но уже загружая её с помощью функции load_model()
из файла "plane_model.hrz". Преимущество этого подхода в том, что модель не должна быть "зашита" в код программы, и можно будет загружать и отображать любые модели:
#include "graphics.h"
#include "input.h"
#include "world.h"
#define DAMPING 5
int main_cycle;
int t;
int main()
{
init_player_input(quit_game);
main_cycle = 1;
struct Model3d model;
load_model("res/models/plane_model.hrz", &model);
init_graphics();
while(main_cycle)
{
init_screen();
int theta = t / DAMPING;
init_screen();
for (int i = 0; i < model.size; i++)
{
struct Triangle3d tmp;
tmp.p1 = rotate_Point3d_around_y(model.triangles[i].p1, theta);
tmp.p2 = rotate_Point3d_around_y(model.triangles[i].p2, theta);
tmp.p3 = rotate_Point3d_around_y(model.triangles[i].p3, theta);
draw_triangle3d(tmp, '*');
}
render_scene();
t++;
}
free_model(&model);
finalize_graphics();
return 0;
}
Дело осталось за малым: реализовать саму функцию load_model()
. Функция load_model()
занимается заполнением структуры Model3d
, открывая нужный файл и считывая его. И, так как при конструировании структуры Model3d
выделяется динамическая память - нужно создать парную функции load_model()
функцию free_model()
, которая будет выделенную память освобождать. Теперь каждый раз, создавая трёхмерный объект, нужно держать в голове необходимость уничтожения объекта. Собственно необходимость программиста вручную следить за выделением и освобождением памяти (ручное управление памятью) является одним из главных минусов языка Си, однако мощь этой возможности такова, что она превышает этот минус. Ну а виноват, если что - программист. Всегда освобождайте память!
load_model()
возвращает загруженную из файла модель. Возврат модели из функции происходит не через возвращаемое значение функции, как этого стоило бы ожидать - переменную для хранения модели необходимо создать заранее, в вызывающем функцию коде, а сама функция load_model()
служит для заполнения структуры в соответствии с данными, прочитанными из файла. Подобный подход - с возвратом результата работы функции через аргумент-ссылку - очень распространён в языке Си, и практически не встречает отторжения у программистов (привыкших и зачастую не предполагающих, что может быть иначе), хотя вообще, и в математике в частности аргументы функции - это именно входные данные, на основании которых производится вычисление, сами же входные данные в результате вычислений изменяться не должны. Результат работы функции должен возвращаться возвращаемым значением функции. В языке же Си возвращаемое значение построенных подобным образом функций зачастую служит признаком корректности отработки функции: например в нашем случае функция возвращает 1, подразумевая что удалось загрузить и преобразовать к виду, понятному программе модель из переданного файла (хотя пока что функция не проверяет, удалось ли ей отработать корректно). Также пока что функция умеет загружать только модели, содержащие 28 треугольников - просто для облегчения примера. И работает она с нашим специфичным форматом хранения моделей, который мы решили сохранять в текстовых файлах формата .hrz.
Хотя размер модели в 28 треугольников на модель мог бы быть фиксированным для нашего формата, или специфичным для разрабатываемого движка (до 90-х годов - из-за технической реализации, а позже - из-за лени программистов), в нашем случае такой размер обусловлен исключительно "игрушечностью" задачи, чтобы не перегружать пример кода. Как сделать возможность загружать модели произвольного размера? Существует множество способов сделать это, но все они (в языке Си) приводят к необходимости использования динамической памяти. Нужно каким-то образом узнать, сколько памяти займёт модель, запросить это количество у операционной системы - и считать в выделенную память модель. Либо можно не узнавать требуемую память заранее, а запросить у операционной системы памяти "чуть-чуть", заполнить её - а потом по необходимости запрашивать всё новую и новую память. Размер модели можно было бы писать самой первой строчкой в файле - тогда парсеру достаточно считать это значение, выделить нужное количество памяти, а далее считывать значение как и в уже приведённом выше коде. Но, чтобы не добавлять в файл избыточных строк, другая возможность узнать размер - просто пройтись по файлу посчитать количество треугольников, а потом уже пройтись второй раз и считать их:
... int load_model(char *path, struct Model3d *model) { int size; FILE *fp; fp = fopen(path, "r"); /* First pass, count file entry - point coordinates */ float tmp; while (fscanf(fp, "%f", &tmp) != EOF) { size++; } rewind(fp); float *triangles = calloc(sizeof(float), size); /* Second, fill vertices */ for (int i = 0; i < size; i++) { fscanf(fp, "%f ", &triangles[i]); } fclose(fp); (*model).size = size; (*model).triangles = (struct Triangle3d*)triangles; return 1; } ...
size
не задаётся жёстко в коде, а высчитывается из количества значений в файле. Первый раз поток считывается "впустую", во временную переменную tmp
- значение которой никак не используется. После того, как весь поток прочитан - и, соответственно, все значения подсчитаны - в потоке возникает ситуация "Конец файла" (EOF
). Поток выдал всё содержимое файла, и в нём больше ничего не осталось. Чтобы вновь установить поток на начало файла, можно воспользоваться функцией rewind()
. В остальном функция load_model()
работает как и прежде.
Можно обраить внимание на количество точек в файле .hrz. В настоящий момент каждый треугольник описывается тремя точками - три тройки значений на каждый треугольник. Это простой способ задания координат тругольников, но не самый эффективный с точки зрения итогового размера файла модели. Если учесть, что в моделях в основном все треугольники составлены "встык", то можно заметить, что обычно одна точка-вершина входит в несколько треугольников, возможно в три или даже более. С учётом этого можно записывать модели иначе - сначала перечислить все вершины, а потом описать треугольники, перечисляя не координаты вершин, а их индексы:

Тогда в файле модели можно выделить только уникальные вершины, присвоив им индексы:
01:{ 0.0 -0.2 -0.6} 02:{-0.1 -0.1 -0.7} 03:{ 0.1 -0.1 -0.7}
04:{ 0.0 0.0 -0.6} 03:{ 0.1 -0.1 -0.7} 02:{-0.1 -0.1 -0.7}
04:{ 0.0 0.0 -0.6} 05:{-0.1 0.0 -0.2} 06:{ 0.1 0.0 -0.2}
01:{ 0.0 -0.2 -0.6} 07:{ 0.2 -0.2 -0.3} 08:{-0.2 -0.2 -0.3}
09:{ 0.0 -0.1 0.8} 08:{-0.2 -0.2 -0.3} 07:{ 0.2 -0.2 -0.3}
04:{ 0.0 0.0 -0.6} 06:{ 0.1 0.0 -0.2} 03:{ 0.1 -0.1 -0.7}
04:{ 0.0 0.0 -0.6} 02:{-0.1 -0.1 -0.7} 05:{-0.1 0.0 -0.2}
10:{ 0.0 0.1 -0.1} 06:{ 0.1 0.0 -0.2} 05:{-0.1 0.0 -0.2}
10:{ 0.0 0.1 -0.1} 11:{ 0.0 0.0 0.6} 06:{ 0.1 0.0 -0.2}
10:{ 0.0 0.1 -0.1} 11:{ 0.0 0.0 0.6} 05:{-0.1 0.0 -0.2}
12:{ 0.7 -0.1 0.0} 06:{ 0.1 0.0 -0.2} 11:{ 0.0 0.0 0.6}
13:{-0.7 -0.1 0.0} 05:{-0.1 0.0 -0.2} 11:{ 0.0 0.0 0.6}
12:{ 0.7 -0.1 0.0} 06:{ 0.1 0.0 -0.2} 14:{ 0.7 -0.1 -0.2}
13:{-0.7 -0.1 0.0} 05:{-0.1 0.0 -0.2} 15:{-0.7 -0.1 -0.2}
12:{ 0.7 -0.1 0.0} 14:{ 0.7 -0.1 -0.2} 16:{ 1.0 0.1 -0.3}
13:{-0.7 -0.1 0.0} 15:{-0.7 -0.1 -0.2} 17:{-1.0 0.1 -0.3}
07:{ 0.2 -0.2 -0.3} 11:{ 0.0 0.0 0.6} 06:{ 0.1 0.0 -0.2}
08:{-0.2 -0.2 -0.3} 11:{ 0.0 0.0 0.6} 05:{-0.1 0.0 -0.2}
07:{ 0.2 -0.2 -0.3} 06:{ 0.1 0.0 -0.2} 03:{ 0.1 -0.1 -0.7}
08:{-0.2 -0.2 -0.3} 05:{-0.1 0.0 -0.2} 02:{-0.1 -0.1 -0.7}
07:{ 0.2 -0.2 -0.3} 03:{ 0.1 -0.1 -0.7} 01:{ 0.0 -0.2 -0.6}
08:{-0.2 -0.2 -0.3} 02:{-0.1 -0.1 -0.7} 01:{ 0.0 -0.2 -0.6}
07:{ 0.2 -0.2 -0.3} 04:{ 0.0 0.0 -0.6} 09:{ 0.0 -0.1 0.8}
08:{-0.2 -0.2 -0.3} 04:{ 0.0 0.0 -0.6} 09:{ 0.0 -0.1 0.8}
18:{ 0.0 0.0 0.9} 04:{ 0.0 0.0 -0.6} 19:{ 0.3 0.0 0.9}
18:{ 0.0 0.0 0.9} 04:{ 0.0 0.0 -0.6} 20:{-0.3 0.0 0.9}
18:{ 0.0 0.0 0.9} 09:{ 0.0 -0.1 0.8} 04:{ 0.0 0.0 -0.6}
18:{ 0.0 0.0 0.9} 11:{ 0.0 0.0 0.6} 21:{ 0.0 0.3 0.9}
И информацию в файле с моделью можно представить как-то навроде следующего примера:
Vertices: Triangles:
01:{ 0.0 -0.2 -0.6} 01 02 03
02:{-0.1 -0.1 -0.7} 04 03 02
03:{ 0.1 -0.1 -0.7} 04 05 06
04:{ 0.0 0.0 -0.6} 01 07 08
05:{-0.1 0.0 -0.2} 09 08 07
06:{ 0.1 0.0 -0.2} 04 06 03
07:{ 0.2 -0.2 -0.3} 04 02 05
08:{-0.2 -0.2 -0.3} 10 06 05
09:{ 0.0 -0.1 0.8} 10 11 06
10:{ 0.0 0.1 -0.1} 10 11 05
11:{ 0.0 0.0 0.6} 12 06 11
12:{ 0.7 -0.1 0.0} 13 05 11
13:{-0.7 -0.1 0.0} 12 06 14
14:{ 0.7 -0.1 -0.2} 13 05 15
15:{-0.7 -0.1 -0.2} 12 14 16
16:{ 1.0 0.1 -0.3} 13 15 17
17:{-1.0 0.1 -0.3} 07 11 06
18:{ 0.0 0.0 0.9} 08 11 05
19:{ 0.3 0.0 0.9} 07 06 03
20:{-0.3 0.0 0.9} 08 05 02
21:{ 0.0 0.3 0.9} 07 03 01
08 02 01
07 04 09
08 04 09
18 04 19
18 04 20
18 09 04
18 11 21
Подобное представление, конечно, не сильно подходит для парсинга - файл стоит переписать, используя более простое и однозначное форматирование. Индексы вершин можно не указывать - считать вершины просто по порядку, в котором они встречаются в файле. Чтобы знать, в какой строке записаны вершины, можно эту строку начинать с символа v - vertex, вершина. Строки с треугольниками c f - face, грань. Также можно условиться остальные строки просто игнорировать. Преобразованный в соответствии с такими условиями файл будет выглядеть так:
v 0.0 -0.2 -0.6
v -0.1 -0.1 -0.7
v 0.1 -0.1 -0.7
v 0.0 0.0 -0.6
v -0.1 0.0 -0.2
v 0.1 0.0 -0.2
v 0.2 -0.2 -0.3
v -0.2 -0.2 -0.3
v 0.0 -0.1 0.8
v 0.0 0.1 -0.1
v 0.0 0.0 0.6
v 0.7 -0.1 0.0
v -0.7 -0.1 0.0
v 0.7 -0.1 -0.2
v -0.7 -0.1 -0.2
v 1.0 0.1 -0.3
v -1.0 0.1 -0.3
v 0.0 0.0 0.9
v 0.3 0.0 0.9
v -0.3 0.0 0.9
v 0.0 0.3 0.9
f 01 02 03
f 04 03 02
f 04 05 06
f 01 07 08
f 09 08 07
f 04 06 03
f 04 02 05
f 10 06 05
f 10 11 06
f 10 11 05
f 12 06 11
f 13 05 11
f 12 06 14
f 13 05 15
f 12 14 16
f 13 15 17
f 07 11 06
f 08 11 05
f 07 06 03
f 08 05 02
f 07 03 01
f 08 02 01
f 07 04 09
f 08 04 09
f 18 04 19
f 18 04 20
f 18 09 04
f 18 11 21
Функцию load_model()
нужно изменить соответсвенно: теперь она не просто записывает вершины, но и расставляет их по надлежащим вершинам в треугольниках:
int load_model(char *path, struct Model3d *model)
{
int v_count = 0;
int f_count = 0;
FILE *fp;
fp = fopen(path, "r");
/* First pass, count vertices and triangles in file */
char c;
while (fscanf(fp, "%c", &c) != EOF)
{
if (c == 'v') v_count++;
if (c == 'f') f_count++;
}
rewind(fp);
/* Second, fill vertices structure Array */
struct Point3d *vts = calloc(sizeof(struct Point3d), v_count);
for (int i = 0; i < v_count; i++)
{
fscanf(fp, "v %f %f %f\n", &vts[i].x, &(vts[i].y), &(vts[i].z));
}
/* Third, fill triangle structure array */
struct Triangle3d *tris = calloc(sizeof(struct Triangle3d), f_count);
int v1, v2, v3;
for (int i = 0; i < f_count; i++)
{
fscanf(fp, "f %d %d %d\n", &v1, &v2, &v3);
tris[i].p1 = vts[v1 - 1];
tris[i].p2 = vts[v2 - 1];
tris[i].p3 = vts[v3 - 1];
}
fclose(fp);
free(vts); /* Don't forget to free unnecessary memory */
(*model).size = f_count;
(*model).triangles = tris;
return 1;
}
Получившийся формат файла очень похож на существующий формат .obj. Можно научить наш парсер загружать .obj-файлы, но для этого нужно обеспечить возможность пропусткать любые другие строки, начинающиеся не с символов v и f. Для этого реализуем функцию set_fstream()
, которая будет перематывать переданный ей поток на строку, начинающуюся с символа, равного второму аргументу функции.
Формат .obj в строках, начинающихся с f хранит не только индексы вершин граней, но и некоторую другую (например, индексы нормалей). Мы эту информацию пока что не используем, поэтому пока что нужно научиться её отбрасывать: начинающиеся с f строки выглядят в .obj описывают "грани", полигоны - тройками чисел f 3/4/5 2/4/1 1/4/2
. В каждой тройке нас пока что интересуют первые числа - это и есть индексы вершин полигона. Парсер пока что будет игнорировать остальные числа. Также нужно помнить, что полигоны в .obj могут иметь и более трёх вершин - наш движок такие ситуации обрабатывать не умеет (и вообще создавать 3д-модели с нетреугольными полигонами в геймдеве не принято уже с начала века). Нужно учитывать это при подготовке модели, и гарантировать что все полигоны будут треугольными (иначе движок не сможет их корректно отобразить).
...
/* Sets FILE stream after 'sample' char and whitespace, or EOF */
int set_fstream(FILE *fp, char sample)
{
char prev = '\n';
char curr = fgetc(fp);
char next = fgetc(fp);
while (!feof(fp))
{
if (prev == '\n' && curr == sample && next == ' ')
{
return 1;
}
prev = curr;
curr = next;
next = fgetc(fp);
}
return 0;
}
int load_model(char *path, struct Model3d *model)
{
int v_count = 0;
int f_count = 0;
FILE *fp;
fp = fopen(path, "r");
/* First pass, count vertices and triangles in file */
while (set_fstream(fp, 'v'))
{
v_count++;
}
rewind(fp);
/* Second, fill vertices structure Array */
struct Point3d *vts = calloc(sizeof(struct Point3d), v_count);
for (int i = 0; set_fstream(fp, 'v'); i++)
{
fscanf(fp, "%f %f %f", &vts[i].x, &vts[i].y, &vts[i].z);
}
rewind(fp);
/* Third, count triangles in file */
while (set_fstream(fp, 'f'))
{
f_count++;
}
rewind(fp);
/* Last pass, fill triangle structure array */
struct Triangle3d *tris = calloc(sizeof(struct Triangle3d), f_count);
int v1, v2, v3;
char ts[20]; /* temporary string - dummy filler for fscanf() */
for (int i = 0; set_fstream(fp, 'f'); i++)
{
fscanf(fp, "%d%s %d%s %d%s", &v1, ts, &v2, ts, &v3, ts);
tris[i].p1 = vts[v1 - 1];
tris[i].p2 = vts[v2 - 1];
tris[i].p3 = vts[v3 - 1];
}
/* Close resources, prepare result */
fclose(fp);
free(vts); /* Don't forget to free unnecessary memory */
(*model).size = f_count;
(*model).triangles = tris;
return 1;
}
...
Теперь можно создавать в вашем любимом 3д-редакторе (рекомендую, конечно, blender) модель и загружать её вместо нашей старой, созданной вручную модели самолёта:
...
int main()
{
....
load_model("res/models/suzanna.obj", &model);
....
}
Углублённые задания для самостоятельной работы:
- Освойте 3D-моделирование.
- Сохраните свои модели в формате .obj, загрузите их в программу. Реализуйте возможность загрузки нескольких моделей.
- В парсере отсутсвует контроль ошибок. При этом файловые операции очень и очень ошибкам подвержены - файла может не существовать, он может не открыться, он может не соответсвтвовать оговорённому формату. Дополните парсер так, чтобы он определял, получилось ли ему корректно считать запрошенный файл, и мог известить вызывающий код о том, корректно ли удалось отработать.