УДК 004.921

К ВОПРОСУ ОБ ИМПОРТЕ 3D МОДЕЛЕЙ В ПРОГРАММЫ С ИСПОЛЬЗОВАНИЕМ ГРАФИЧЕСКОЙ БИБЛИОТЕКИ OPENGL

Котов Вадим Викторович1, Абрамова Оксана Федоровна1
1Волжский политехнический институт (филиал) ФГБОУ ВПО "Волгоградский государственный технический университет", г. Волжский, Россия

Аннотация
Данная статья посвящена исследованию проблемы импорта 3D-моделей из графических редакторов в программный код с использованием графической библиотеки OpenGL. Предложен алгоритм решения проблемы, выполнен подробнейший анализ алгоритма и программной реализации, рассмотрены достоинства и недостатки описываемого метода.

Ключевые слова: алгоритм, импорт, компьютерная графика, конвертер, программная реализация


ON THE IMPORT 3D MODELS IN A PROGRAM USING OPENGL GRAPHICS LIBRARY

Kotov Vadim Viktorovich1, Abramova Oksana Fedorovn1
1Volzhskiy Polytechnical Institute, branch of the Volgograd State Technical University, Volzhskiy, Russia

Abstract
This article deals with the problem of import of 3D-models of graphical editors in code using the graphics library OpenGL. An algorithm is proposed to solve the problem, a detailed analysis of the algorithm and software implementation, considered the advantages and disadvantages of the method described.

Keywords: 3D-моделирование, OpenGL


Библиографическая ссылка на статью:
Котов В.В., Абрамова О.Ф. К вопросу об импорте 3D моделей в программы с использованием графической библиотеки OpenGl // Современная техника и технологии. 2014. № 1 [Электронный ресурс]. URL: https://technology.snauka.ru/2014/01/2965 (дата обращения: 15.07.2023).

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

Когда человек начинает писать программу с использованием инструментов графической библиотеки OpenGl, то рано или поздно он понимает, что стандартных средств интерфейса совершенно недостаточно для построения графического объекта хоть сколько-нибудь приемлемой сложности. Много ли можно сделать из прямоугольников и треугольников? Много, совершенно справедливо ответите вы. И будете правы, ибо полигональные модели как раз и формируются из этих самых треугольников. Собственно, полигон – это и есть многоугольник, причем чаще всего используются как раз треугольники или прямоугольники[2]. Вот только вручную просчитать расположение сотен (а то и тысяч, и сотен тысяч) полигонов – занятие, конечно, реализуемое, но маловероятное. Нет, конечно же, еще фигуры можно задавать массивами координат, но высчитать подобный массив – задача не намного легче.

Как поступает студент в таком случае? Он использует 3D-редакторы для отрисовки сложных 3D-объектов. 3D-редакторы обладают удобным графическим интерфейсом, интуитивно понятным даже для новичка, и научиться создавать с их помощью достаточно сложные объекты, а затем переводить их в понятные для OpenGl списки координат, не составит для увлеченного человека особого труда. И вот тут разработчика ожидает серьезная проблема: к сожалению, OpenGl не содержит встроенных средств для чтения 3D-моделей. Поэтому  реализовывать такое приложение приходится самому.

Нет, с одной стороны, координаты – это всегда координаты, и вроде бы ничего сложного в переносе нет. В крайнем случае, можно взять массив координат, полученный в редакторе, и вручную вставить его в код. Можно даже перенести 10 тысяч полигонов из 3D-редактора в код. Но сколько времени займет данная операция?

Для решения этой проблемы можно избрать следующий путь:  привлечь «автозамену», встроенную в любой текстовый редактор, и с помощью неё постараться подогнать форматы. Но это тоже не избавит вас от огромной ручной работы. Да и «автозамена» никогда не была интеллектуальным алгоритмом, и гарантии, что после такой длительной работы у вас все заработает, никто не даст.

Следовательно, скажет пытливый читатель, процесс преобразования надо автоматизировать. И, разумеется, будет в чем-то прав. Такой вывод напрашивается сам собой, поэтому обойти стороной такое решение мы не могли. Вот только реально эта задача труднореализуема: в интернете с большим трудом, но все-таки можно найти весьма сложные встраиваемые комплексы, которые после включения в ваш программный код динамически загружают и обрабатывают модели. Так вот же решение, скажете вы: и код компилируется быстро (массив координат содержится вне кода, и в компиляции не участвует), и новую модель подключить не сложно. И будете не совсем правы. Потому как данный подход  обладает рядом довольно весомых недостатков. Самый важный из них – сложность данного способа. Для человека, который не слишком хорошо разбирается в объектно-ориентированном стиле программирования  (т.к. большинство этих способов реализованы именно в виде подключаемых классов), понять, что делает подобный загрузчик, чрезвычайно трудно. Да и объем у подобного загрузчика весьма велик. А если я просто хочу сделать вращающийся стул? При этом код всей программы будет раз в 10 меньше кода загрузки модели, и это ставит под серьезные сомнения возможность применения такого подхода. И для начинающих его вряд ли можно посоветовать.

Поэтому мы пойдем другим путем. Мы разработаем несложный алгоритм преобразования нашего файла с координатами в код, спокойно читаемый OpenGl. И лучше всего будет оформить данный алгоритм в отдельную программу, которая будет конвертировать необходимые модели для реализации их с помощью графической библиотеки OpenGl.

Рассмотрим процесс создания алгоритма для конвертирования 3D-моделей из 3D-редактора в программный код. Для начала выберем исходный формат файла-источника. Предлагаю использовать OBJ-формате, и для этого есть две веские причины. Во-первых, это один из самых распространённых форматов хранения данных, а во-вторых (и это главное в нашем случае), он текстовый и, как следствие, более понятный для восприятия.

Но для начала немного уточним терминологию. Как в OpenGl, так и в OBJ есть координаты и индексы [3]. Координата – это расположение точки на виртуальной координатной плоскости в программе (по трем измерениям). Тут все просто. А вот с понятием «индекс» могут возникнуть сложности.  В нашем случае «индекс» – это номер координаты в массиве координат. Поясним ситуацию на примере: допустим, дан массив из 4-х наборов координат по трем осям X, Y, Z:

          (x, y, z)

    1)    1,  3, 5

   2)   -2, 7, 9

    3)   -5, 4, -7

     4)    8, -6, -1

И координата некоторой точки, указанная в индексах:

Точка 1: 4, 3, 2

В этом случае значения индексов точки означают номер ряда из массива, по соответствующей оси. В нашем случае, индекс по оси X равен 4, следовательно, необходимо взять координату оси Х из четвертого ряда (у нас это 8). Точно так же восстанавливаем координаты по оси Y (третий ряд, вторая цифра – 4) и Z (второй ряд, третья цифра – 9).

В итоге получаем, что координаты точки, заданной с помощью индексов будут следующие:

Точка 1: 4, 8, 9

Подобный метод индексации необходим для сокращения количества координат. Ведь у многогранных фигур очень часто встречаются либо общие точки, когда совпадают все три координаты, либо точки, находящиеся на одной линии, у которых общие одна из координат.

Также необходимо учитывать, что для описания модели используют три типа координат: координаты модели (то есть самого объекта), координаты нормалей (необходимы для распределения освещенности в сцене) и координаты текстур (при наличии текстур в модели).

Теперь пару слов непосредственно про формат OBJ. Информация в нем хранится в текстовом виде, что позволяет ее просмотреть с помощью любого текстового редактора. Во многих подобных файлах помимо координат хранятся еще и ссылки к материалам, текстурам и др. Стандартный вид obj-файла следующий:

# Max2Obj Version 4.0 Mar 10th, 2001

v  10.4335  217.914  7.80485

v  -8.02548  219.079  6.90515

v  -8.22437  219.042  6.90515

v   4.414604  70.340584  7.308666

# 4 vertices

vn  0.000000 0.000002 -1.000000

vn  -1.000000 0.000000 0.000000

vn  0.000000 0.000000 1.000000

# 3 vertex normal

vt  0.47683 0.51250

vt  0.46556 0.50879

vt  0.46335 0.50876

# 3 texture vertices

f  1/1/1  1/2/1  2/3/1

f  4/2/1  3/1/3  4/1 1

f 3/1/3  2/3/1  3/1/2

# 3 faces

Разберем данный пример подробнее. Первая строка представляет собой комментарий, содержащий не интересующую нас общую информацию. Начиная со второй строки располагается массив координат модели (обозначается латинской V).

v  10.4335  217.914  7.80485

v  -8.02548  219.079  6.90515

v  -8.22437  219.042  6.90515

v   4.414604  70.340584  7.308666

Это описание каркаса нашей модели (точнее, массив координат каркаса, помним про индексы). Он есть в любом файле.

Затем указаны координаты нормалей:

vn 0.000000 0.000002 -1.000000

vn -1.000000 0.000000 0.000000

vn 0.000000 0.000000 1.000000

Координаты нормалей никогда не принимают значение большее единицы и указывают направление света. Указываются в файлах далеко не всегда: нет освещенности, нет и нормалей.

Далее перечисляются координаты текстуры:

vt 0.47683 0.51250

vt 0.46556 0.50879

vt 0.46335 0.50876

Поскольку текстура сама по себе двухмерная, третья координата обычно либо не указывается, либо равна 0 (vt 0.46335 0.50876 0). Данного блока может не быть, если у модели нет текстур.

Зачастую между блоками встречаются комментарии с указанием количества этих самых блоков ( # 3 texture vertices). Также можно уточнить, что количество точек каркаса, текстур и нормалей не всегда соответствует друг другу. Это совершенно нормально и  приводится в соответствие с помощью индексов.

Какую же полезную информацию может почерпнуть разработчик из представленных массивов данных?

1. В пределах одной модели порядок массивов координат всегда сохраняется: сначала указывается блок V (координаты моделей), потом блок VT (текстуры), затем блок VN (нормали). Блоки никогда не перемешиваются и не меняются местами (то есть, блока текстур может не быть вообще, но если он есть, то идет всегда после массива координат каркаса).

2. Между собой данные разделяются пробелом.

Эти два факта мы будем активно использовать  для реализации конвертера 3D-моделей.

Но это еще не все содержимое obj-файла. За блоком текстур располагается блок, строки которого начинаются с буквы f:

f  1/1/1  1/2/1  2/3/1

f  4/2/1  3/1/3  4/1 1

f  3/1/3  2/3/1  3/1/2

Это массив индексов. И составные элементы этого блока очень важны для понимания работы алгоритма конвертера, поэтому мы рассмотрим их подробнее. Буква f, как уже было сказано,  обозначает начало строки с индексами. Далее идут числа с разделителями, значения которых расшифровываются следующим образом: пробелом разделяются оси (соответственно, блоков чисел всегда 3: ось х, ось y, ось z), а внутри каждого блока допускается существование от 1 до 3 цифр, разделенных знаком «/» (или «//»):

f  1//1//1  1//2//1  2//3//1

f  4 3 4

f  3/1  2/2  3/1

Каждая тройка чисел несет следующую информацию: индекс каркаса//индекс текстуры//индекс нормали. Соответственно, если у модели нет текстур или нормалей, то и соответствующих индексов в блоке не будет.

Например,  рассмотрим одну из точек, строка индексов которой указана последней в блоке f:  f  3/1/3  2/3/1  3/1/2. Индекс координат данной точки: 3, 2, 3 (первые цифры каждого блока), следовательно, значения её координат мы выбираем из массива координат модели v из третьего, второго и снова третьего ряда: -8.22437,   219.079, 6.90515. Индекс текстур точки: 1, 3, 1 (вторые цифры блока). Следовательно, текстурные координаты данной точки 0.47683, 0.50876. Нормали выбираются по тому же принципу.

Также важно помнить,  что в одном файле могут быть указаны описания нескольких моделей, каждое из которых состоит из вышеперечисленных блоков.

Рассмотрим  теперь способ записи координат в OpenGl проекте.  В общем виде описание изображения с использованием массивов выглядит, например, так:

glEnableClientState(GL_VERTEX_ARRAY);

glEnableClientState(GL_NORMAL_ARRAY);

glEnableClientState(GL_TEXTURE_COORD_ARRAY);

GLfloat pVerts1[]= {

-610.2380, 0.0000, 1368.7837,

-610.2380, 0.0000, 504.9502

};

GLfloat pNorm1[]= {

0.0000, -1.0000, -0.0000,

0.0000, 1.0000, -0.0000,

0.0000, 0.0000, 1.0000

};

GLfloat pTexCoord1[]= {

1.0000, 1.0000,

0.0000, 0.0000,

};

glVertexPointer(3, GL_FLOAT, 0, pVerts1);

glNormalPointer(GL_FLOAT, 0, pNorm1);

glTexCoordPointer(2,GL_FLOAT, 0,  pTexCoord1);

glDrawArrays(GL_TRIANGLES,0,3);

glDisableClientState (GL_VERTEX_ARRAY);

glDisableClientState (GL_NORMAL_ARRAY);

glDisableClientState (GL_TEXTURE_COORD_ARRAY);

Первые три строки необходимы для указания того, что будут использоваться 3 массива: массив вершин (GL_VERTEX_ARRAY),массив  нормалей (GL_NORMAL_ARRAY) и массив текстур (GL_TEXTURE_COORD_ARRAY). Далее указывается непосредствнно массив координат модели pVerts1[], который, в отличие от блока в OBJ, хранит уже готовые координаты, по которым строится каркас (напомним, что в OBJ каркас модели строился по индексам). При этом координаты в массиве pVerts1[] разделяются не пробелом, как в obj-файле, а запятыми. Затем инициализируются массив нормалей pNorm1[] и массив текстур pTexCoord1[].

Следующие три строки необходимы для указания, во-первых, какой из массивов содержит какие конкретно значения, а, во-вторых, каким способом эти значения из массива надо брать:

  • glVertexPointer(3, GL_FLOAT, 0, pVerts1) –  определяем, что массивом вершин будет именно массив pVerts1, определяем тип значений этого массива «float», и количество координат на одну вершину (по три, то есть три оси);
  •  glNormalPointer(GL_FLOAT, 0, pNorm1) –  массив нормалей называется pNorm1 и также хранит значения типа «float»;
  •  glTexCoordPointer(2,GL_FLOAT, 0,  pTexCoord1) – текстурный массив с именем pTexCoord1 и количеством переменных на точку – 2 (текстуры у нас двумерные).Далее переходим к отрисовке самой модели: glDrawArrays (GL_TRIANGLES,0,3). Здесь GL_TRIANGLES указывает, что рисовать будем треугольники; 0 – выборку координат из массива начнем  с нулевого элемента; 3 – на одну вершину будем считывать по три значения. Данная команда выводит треугольники по нашим координатам, пока они (координаты) не кончатся. Последние три строки примера необходимы для отключения установленных режимов обработки массивов. Эту функцию  необходимо  задавать в конце всего процесса вывода, но не обязательно для каждой модели.Перейдем теперь непосредственно к процессу перевода одного формата в другой. При этом при создании OBJ файла по умолчанию считаем, что отрисовывать будем треугольники. Не будем останавливаться на таких проблемах, как разные разделители координат (в файлах пробелы, в коде запятые), или отличающийся вид самих массивов (в файле каждая строка обозначается буквами, в коде достаточно фигурной скобки в начале блока), и прочем. Это можно достаточно просто исправить простейшими текстовыми преобразованиями (типа «автозамены»). Но вот об одной проблеме поговорим подробнее.

    Как уже говорилось выше, в obj-формате все точки хранятся не в чистых координатах, а в массивах индексов. Библиотека OpenGl такого описания не понимает. Вернее, понимает, но массив индексов при построении фигуры можно использовать только один – координат каркаса, а вот нормали и текстуры будут проигнорированы.  Как следствие, если нам нужен только каркас без всего остального, работа сильно упрощается. Но мы сейчас говорим о полноценном построении трехмерной модели, поэтому нам важны все три массива значений.

    Итак, для начала нам требуется считать файл. Чтение файла происходит построчно. При этом индексы в конце файла указывают на массив координат в его начале. Появляется проблема: или перечитывать файл на каждый индекс (что не слишком эффективно, учитывая, что точек – тысячи), либо сначала считывать и обрабатывать файл (и распределять по массивам в памяти), а потом уже формировать код. Поэтому выбираем второй способ. Так же необходимы места для хранения считанного массива. Будем использовать двухмерные массивы (код на Си):

    float coord_vert [1000000][2];

    float coord_ norm [1000000][2];

    float coord_ tex [1000000][2];

    Здесь мы объявили три массива типа «float», в которых будем хранить массивы вершин, нормалей и текстур соответственно. Количество элементов в массиве: миллион строк (что для высокополигональных моделей имеет смысл) и по 3 элемента в каждой строке (оси X,Y,Z). По умолчанию будем считать, что первая цифра – это номер самого массива, вторая – номер элемента в этом массиве.

    Таким же образом создаем массивы для хранения трех индексов, но с учетом того, что индексы у нас целочисленные (тип int):

    int  ind_vert [1000000][2];

    int  ind _ norm [1000000][2];

    int  ind _ tex [1000000][2];

    Тогда конвертация трехмерной модели будет проходить в два этапа:

  1. считывание и обработка исходного файла;
  2. построение файла с кодом.На первом этапе мы считываем одну модель и распределяем её по массивам. Далее осуществляем обработку массивов по отдельности: массивов координат и массивов индексов. Сначала ищем в строке символы “v”, “vt”, “vn”,  и, в случае обнаружения, следующие 3 координаты записываем в соответствующий массив под соответствующими номерами. И увеличиваем номер этого массива на 1.Когда мы доходим до массива индексов (символ “f”), мы должны отключить поиск координат (если этого не сделать, то к нам может попасть массив координат от следующей модели). Считывание индексов происходит по следующему алгоритму:
  1. читаем первое число за пробелом, вносим его в массив индексов  каркаса (под соответствующим номером) как первый элемент массива (координата Х);
  2. проверяем на наличие знака разделителя: «/» или «//» (т.к.  мы не знаем вид исходного файла);
  3. если знак разделителя найден, то проверяем, есть ли у нас структура с координатами текстур (как  уже говорилось ранее, в индексах координаты идут по порядку (вершины/текстуры/нормали), но если текстур нет, то и текстурных индексов соответственно не будет, и без этой проверки мы можем выполнить запись индексов «не по адресу»);
  4. записываем число после знака разделителя в массив с индексами текстур (или нормалей, если текстур нет) в первый элемент массива;
  5. снова проверяем на наличие разделителя, и, при его обнаружении,  формируем массив нормалей;
  6. выполняем поиск пробела;
  7. число, следующее за пробелом, записываем в массивы с тем же номером и в том же порядке (сначала вершины, потом текстуры, потом нормали), но уже во второй элемент массива (т.е. формируем координаты оси Y);
  8. повторяем те же действия для оси Z.Выполняем вышеуказанные действия до обнаружения знака начала комментариев (#), новых координат вершин (v) или конца файла. Затем  осуществляем  вывод нужного кода в следующем порядке:
  • сначала выводим оформление кода: GLfloat pVerts1[]= {
  • затем выводим координаты (по три в строке,  что удобнее для восприятия).Вывод координат осуществляется следующим образом: запускаем цикл (от нуля до того количества индексов, которое мы записали); выводим первый элемент массива; в качестве номера самого массива берем значение из массива индексов:for (int i=0; i<kol_vo_indexov_vert; i++)

    coord_vert [(ind_vert[i][0])-1] [0] «,»

    coord_vert [(ind_vert[i][1])-1] [1] «,»

    coord_vert [(ind_vert[i][2])-1] [2]  «,»

    В качестве индекса текущего элемента указываем формулу, с помощью которой можно будет сортировать значения: второй индекс равен 0, следовательно,  мы работаем с осью Х;  для оси Y второй индекс будет равен 1, а для оси Z – 2. Вычитание же единицы из индекса производится потому, что в Си индексация элементов массива начинается с нуля,  а в ob- формате – с единицы. Таким образом, происходит построение нового файла с кодом, включая вывод блоков текстур и  нормалей.

    Затем в конвертер 3D-моделей добавляется вывод:

    glVertexPointer(3, GL_FLOAT, 0, pVerts1);

    glNormalPointer(GL_FLOAT, 0, pNorm1);

    glTexCoordPointer(2,GL_FLOAT, 0,  pTexCoord1);

    glDrawArrays(GL_TRIANGLES,0,3);

    И осуществляется проверка достижения конца файла. В случае неуспеха, означающего, что в файле есть описания других моделей, все действия повторяются для остальных моделей.

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


Библиографический список
  1. Абрамова, О.Ф. Использование мультимедийных технологий в процессе обучения дисциплине “Компьютерная графика” / Абрамова О.Ф., Белова С.В. // Успехи современного естествознания. 2012. № 3. C. 90.
  2. Абрамова, О. Ф. Компьютерная графика. Избранные лекционные темы. Ч. 1/ Абрамова О. Ф.// Saarbrucken : Palmarium Academic Publishing. 2012.
  3. http://ru.wikipedia.org/wiki/Obj.


Все статьи автора «AbramovaOF»


© Если вы обнаружили нарушение авторских или смежных прав, пожалуйста, незамедлительно сообщите нам об этом по электронной почте или через форму обратной связи.

Связь с автором (комментарии/рецензии к статье)

Оставить комментарий

Вы должны авторизоваться, чтобы оставить комментарий.

Если Вы еще не зарегистрированы на сайте, то Вам необходимо зарегистрироваться: