Экспорт "сложных" сцен из Blender в свой движок на Delphi.
главная страница статьи файлы о сайте ссылки
Экспорт "сложных" сцен из Blender в свой движок на Delphi

Георгий Мошкин
tmtlib@narod.ru

Файлы к статье:
blender10a.rar (413kb) - ихсодники вместе с откомпилированной exe-шной демкой
blender10a-src.rar (253kb) - исходники без exe
sferka10a.rar (232kb) - экспортированная сцена со сферой
models10a.rar (303kb) - сцены в формате Blender

В трёх предыдущих трёх статьях я показал простейший пример по использованию моделей из Blender в своих программах на Delphi. Оказалось, что это весьма просто. Возможно, что вы пробовали экспортировать описанным мною способом что-нибудь "покруче", но вместо нормальной картинки получали белибердень.

В данной статье я не только покажу, как добиться правильной работы экспортера, но ещё раз некоторые элементы по работе с Blender. Научившись работать с Blender вы быстро и просто сможете создавать модели для вашей игры. А если созданием игры занято несколько человек, то знание Blender-а даст вам возможность более продуктивно общаться с моделлером из вашей команды, разъясняя ньюансы по созданию моделей для вашего Квейка или Resident Evil. В конце этой статьи я покажу, что мы можем загрузить в Delphi вот такую милую сцену с красивыми деревянными раскладными стульями, позаимствованную с сайта http://BFight.org:

Создание простейшей модели было рассмотрено в статье "Создание простой модели". Сцена у нас состояла из одного объекта. Как вы помните, это была странная конструкция, которую мы получили последовательным вытягиванием полигонов куба. В данную сцену можно добавить новые объекты. Для этого перейдите в режим "Object Mode" (выпадающее меню со списком режимов находится под окном сцены).

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

Теперь добавим новый объект. Например, пусть это будет сфера. Нажмите правую кнопку мыши и не отпускайте её в течнии нескольких секунд. Как только появится меню, выберите в нём пункт, соответствующий добавлению сферы (Sphere):

Затем появится окошко с вопросами, влияющими на количество полигонов в сфере. Можете просто нажать "ok" в ответ на оба вопроса, не меняя числа:

Теперь в нашей сцене присутствуют два объекта: глыба и сферой.

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

	# export Meshey
	for object in MYOBJECTS:
		if object.getType() != 'Mesh': continue
		print 'mesh:' + object.name
		mesh = object.getData()
        mesh.transform(object.matrix, True)
		faces = mesh.faces

Была добавлена лишь одна строчка с вызовом функции mesh.transform(). Здесь всё просто: сразу после того, как мы получили данные меша с помощью getData(), мы трансформировали все его вершины матрицей object.matrix. Эта матрица содержит все данные о положении объекта в сцене. Второй параметр, отвечающий за пересчёт нормалей, мы сделали равным True. Этот параметр даёт Blender-у понять, что мы хотим получить правильные нормали (Blender заново произведёт рассчёт нормалей). После применения матрицы к объекту, он повернётся и переместится таким образом, что окажется в нужном месте. И, кстати возможно, что произойдут ещё какие-нибудь метаморфорзы. Поэтому и нормали хотелось бы получить обновлённые.

В итоге после применения transform мы получим как раз то, что отображается в Blender на экране - правильную сцену. По сути Blender перед выводом на экран и занимается тем, что применяет матрицу object.matrix к объектам. Сами же объекты в 3D графике часто хранят в "нетронутом" состоянии, так как с этим связаны не только вопросы создания оптимальных алгоритмов, но и решение проблемы накопления ошибки точности при многократных поворотах и перемещениях.

В предыдущей версии экспортера я допустил небольшую ошибку: после обращения hasFaceUV я не поставил скобки. Из-за этого экспортер "вылетал" при экспорте сцен, в которых присутствовали неоттекстурированные объекты. Исправим эту ошибку и добавим запись "кодового слова" NOTEXTURE, которое будет говорить об отсутствии текстуры на объекте.

		if mesh.hasFaceUV(): myfile.write("%s \n" % face.image.name)
		else: myfile.write("NOTEXTURE\n")

Вы не поверите, но это всё, что нам нужно было сделать, чтобы начать экспорт "крутых" сцен. А именно: мы добавили строчку mesh.transform и исправили проверку наличия текстуры. Также на всякий случай я добавил проверку hasFaceUV() перед блоком записи текстурных координат (смотрите исходники скрипта nashformat.py). Осталось лишь немного доработать наш загрузчик моделей на Delphi, чтобы он оределял признак NOTEXTURE.

Всё, что мы сделаем - это добавим к типу TPoligon запись о наличии текстуры hasUV:

type TPoligon=record
               BMPname   : string;
               vertnum   : integer;
               vert      : array[0..3] of TVector;
               norm      : array[0..3] of TVector;
               texcoord  : array[0..3] of TUVCoord;

               gltex     : GLUINT;
               hasUV     : boolean;
              end;

При загрузке мы будем смотреть: если есть нормальное имя текстуры, то делаем hasUV:=true:

       if Poligoni[j-1].BMPname<>'NOTEXTURE' then // текстура есть
         Poligoni[j-1].hasUV:=true;

Если же нам попадётся кодовое слово NOTEXTURE, то признак hasUV останется равен false.

Для наглядности приведу пример - отрывок из файла, который записывает наш экспортер:

face 
NOTEXTURE
4 
-2.211733 -2.206164 -1.308862 
-2.072891 -1.972393 -1.311655 
-2.058106 -1.986658 -1.448910 
-2.193572 -2.214746 -1.446185 
-0.901338 0.422345 -0.095995 
-0.803162 0.587651 -0.097965 
-0.792708 0.577566 -0.195015 
-0.888494 0.416287 -0.193090 

face 
1.bmp 
3 
0.360000 1.000000 1.000000 
-1.000000 -1.000000 1.000000 
0.359999 -1.000000 1.000000 
-0.666667 0.666667 0.333333 
-0.408248 -0.408248 0.816497 
-0.301503 -0.904540 0.301503 
0.000000 1.000000 
1.000000 0.000000 
1.000000 1.000000 

Как видно, здесь два полигона (face). Один полигон четырёхугольный и у него нет текстуры. Второй полигон треугольный, и у него есть текстура. Ну а раз у него есть текстура, то есть и текстурные координаты. Так вот, при загрузке такого файла наш загрузчик выдаст ошибку, потому что будет пытаться считать четыре пары текстурных координат у полигона с NOTEXTURE. Чтобы этого не происходило, загрузку текстурных координат мы будем делать только если hasUV=ture:

   if Poligoni[j-1].hasUV then
    for i:=0 to Poligoni[j-1].vertnum-1 do 
     begin
      readln(f,s);
      s:=trim(s);
      Poligoni[j-1].texcoord[i].u:=StrToFloat(StringWordGet(s,' ',1));
      Poligoni[j-1].texcoord[i].v:=StrToFloat(StringWordGet(s,' ',2));
     end;

Ну и незначительное добавление перед выводом полигонов на экран:

  if poligoni[i].hasUV then  //Если объект текстурирован
    begin
     glEnable(GL_TEXTURE_2D);
     glBindTexture(GL_TEXTURE_2D,poligoni[i].gltex);
    end
     else  // Если объект не текстурирован
    begin
     glDisable(GL_TEXTURE_2D);
    end;

Да, это весьма неоптимизированный вариант. В том плане, что иногда будут происходить лишник вызовы процедур. Здесь я хотел показать именно простоту, а о том, как оптимизировать, вы можете найти в моей статьи про добавление загрузки текстур к half-life smd (в самом конце статьи).

Итак, уже всё готово к загрузке крутых сцен. Чтобы увидеть правильность экспорта нормалей, добавим в наш исходный код какой-нибудь OpenGL-евский источник света. Чтобы особо долго на этом не останавливаться, добавим источник света "по умолчанию" без применения дополнительных параметров:

   glEnable(GL_LIGHTING);
   glEnable(GL_LIGHT0);

Теперь посмотрим, что же выдаст наш загрузчик на экран:

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

Ну посмотрим как будет выглядеть загрузка обещанного примера модели, взятой с сайта http://BFight.org:

Всё отлично грузится! Значит и наш экспортер работает правильно!

Расскажу ещё о нескольких моментах, связанных с экспортом моделей. Как вы понимаете, текстуры не всегда бывают в BMP формате. Наример, в примере со стульями там были и PNG, и TGA. Поэтому и в экспортированных текстовых файлов будут ссылки на текстуры типа chairGround256.tga и т.д.. А наш загрузчик пока умеет загружать только BMP-шки. Чтобы быстро решить эту проблему, я сконвертировал все текстуры в BMP, но имена у них оставил старые. То есть внутри chairGround256.tga сейчас на самом деле находится BMP-шная текстура. Есть много способов обойти эту проблему: это и подключение юнитов с поддержкой разных графических форматов, и анализ текстовой строки (при загрузке менять .tga на .bmp). Да в принципе это и не так важно, ведь задача легко решается.

Ещё немного информации по самому исходному коду. Чтобы сцена на экране вращалась, я добавляю строчки типа этой: glRotatef(gettickcount/100,0,0,1). Понятно, что glRotatef - это OpenGL-евская функция, отвечающая за вращение. Здесь я больше внимания хотел бы уделить именно gettickcount-у. Что делает gettickcount? Возвращает время в миллисекундах. Так почему же вместо угла я подставляю время в миллисекундах? Это очень просто. Как вы знаете, можно повернуть объект на 360 градусов. А можо ещё раз. И это будет уже 360+360 = 720. Получается, что в качестве угла использую время. Это как бы такой большой угол получился, который постоянно увеличивается. Но это вовсе не означает, что OpenGL будет делать сотни и тысячи ненужных поворотов на 360 градусов. Ненужная часть числа отбрасывается автоматически в недрах OpenGL, да и сами функции sin и cos обладают периодичностью, так что им не страшно на вход подавать углы > 360 градусов. Ну а чем быстрее меняется угол, тем быстрее всё крутится. Чтобы сцена крутилась в спокойном ритме, я добавил деление на 100. Итак, gettickcount удобен тем, что его достаточно подставить, если вам нужно какое-то меняющееся число, а вводить новую переменную и всякие t:=t+1 нет времени.

Ну и ещё. В исходниках, которые вы можете скачать с этой страници, я закомментировал источник света. Это связано с тем, что в примере я гружу модель, в которой освещение нарисовано в самой текстуре. Чтобы посмотреть пример со сферой расскоментируйте path:='.\sferka\'; и glEnable(GL_LIGHTING); glEnable(GL_LIGHT0). А также отрегулируйте расстояние до сцены, уменьшив значение на входе glTranslatef(0,0,-45);. С минус 45 можете сделать где-то -25.

Другие вопросы экспорта и исользования в своём движке, к примеру, скелетной анимации и всяких "вкусностей" Blender-а, порою даже проще, чем экспорт самих моделей. На этом данную статью заканчиваю, желаю вам успехов в освоении 3D графики с OpenGL, Delphi и Blender-ом.