главная страница статьи файлы о сайте ссылки |
Обратите внимание - самая новая версия загрузчика моделей Half-Life smd: текущая версия загрузчика SMD. Файлы к статье: КАК ДОБАВИТЬ ПЛАВНУЮ АНИМАЦИЮ читайте здесь (маленькая заметка ПЛЮС пример с исходниками, переделанный под плавную анимацию). Файлы по теме статьи: Скриншот: Георгий Мошкин Пишем свой загрузчик скелетной анимации Half-Life на Borland Delphi. Без использования кватернионов (для простоты). Итак, долго вдаваться в подробности что такое smd файлы я не буду. Скажу лишь, что когда делали игру Half-Life, то модели к ней создавались в 3D Studio MAX. К 3DS MAX существовал плагин (plugin), который записывал модели и скелетную анимацию в текстовые файлы с расширением SMD. Потом кучу таких файлов собирали ("компилировали") в один бинарный с расширением MDL. Формат SMD файлов очень прост и немного похож на формат MilkShape 3D ASCII. Так как смысл в записанных данных в SMD и MilkShape одинаков, то легко написать проигрыватель и для MS3D-совских файлов (милкшейпа). Для начала распотрошим какой-нибудь MDL файл, сделанный для халфы. Для этого можно использовать программу MilkShape 3D (в меню tools выбрать пункт decompile half-life mdl). После этого получится куча текстовых файлов с расширением SMD, среди которых есть один файлов с мешем (mesh/модель), а остальные - с анимацией (sequence). Хотя мне больше нравится утилита MDLDEC.EXE: её нужно просто запустить, указав в коммандной строке в качестве параметра имя какого-нибудь MDL файла. Или можно сделать свою модель и анимацию в программе MilkShape 3D с нуля и через меню file/export записать half-life smd. Ну вот, есть у нас куча SMD файлов, и их надо как-то грузить в программу. Поэтому сначала мы опишем массивы, куда всё это будет загружено. Выше я уже говорил, что есть два типа SMD фалов. Первый тип - это модель. Второй тип - это анимация. Файлов второго типа больше, так как для каждого вида анимации существует отдельный SMD файл: бег, ходьба, стрельба, подпрыгивание, ползание, плавание и т.д.. Таким образом у нас есть множество SMD с анимацией, а модель одна и хранится в одном SMD файле. Раз у нас есть два типа файлов, то целесообразно завести два типа данных: модель и анимация. Назовём эти типы так, чтобы сразу было понятно что это: TModelka и TAnimacia. Ну а в будущем в программе весьма приятно будет написать Models:array of TModelka и Animation:array of TAnimacia. Как видите, здесь всё просто. Начнём с описания типа TModelka. Итак, если мы заглянем в SMD-файл, в котором хранится модель, то вначале увидим следующее: version 1 Версию файла, а также всякие бредовые названия толи костей, толи суставов хранить не будем. Данная структура представляет собой описание связей в скелете (что к чему прикреплено). Без мусора информация о связях в скелете будет выглядеть так: 0,-1 Ну и тут сразу видно, что отлично для хранения этих данных подойдёт одномерный массив. Простой одномерный массив. Делаем юнит modelki.pas. И пишем: unit modelki; Теперь добавляем тип TModelka: type TModelka = object Выше мы выяснили, что для хранения взаимосвязей в скелете подойдёт одномерный массив. Зададим этот одномерный массив внутри нашей модельки: type TModelka=object Смысл здесь таков. Номер элемента массива - это номер сустава, а значение элемента массива - это куда сустав прикреплён (костью, но о костях позже). Посмотрим немножко данных из файла: 0 "Bip01" -1 0,-1 Смысл массива будет такой: svaz[0]:=-1; Нулевой сустав присоединён с помощью кости к мифическому минус-первому суставу (пространство). Первый сустав присоединён к нулевому и т.д. Не обязательно так складно, кости присоединяются по-разному. Например, вот так: 5 "Bip01 DX Coscia"
1 5,1 Короче говоря, не обязательно цифры справа идут по очереди. Это и правильно, потому что никто не запрещает сделать модель, в которой восьмой сустав прикреплён через кость к первому. Ну и можно вдобавок привести смысл заполнения массива в этом случае: svaz[5]:=1; С этими данными мы разобрались. Теперь идём дальше по тексту SMD-файла с моделью. Идём-идём и тут бац! Вот что мы видим: skeleton Какой-то вылез скелетон (skeleton). Что за скелетон? Сразу отметим одну особенность: выше мы видели блок такого вида: nodes Ну а здесь мы нашли блок, который выглядит так: skeleton Особенность заключается в том, что блоки данных имеют что-то вроде begin-а и end-a. В первом случае begin-ом блока является слово nodes, а во втором - skeleton. Блок nodes - информация о взаимосвязях в скелете: какой сустав с каким соединён. А блок skeleton - это информация о том, как перемещены и повёрнуты друг относительно друга суставы скелета! Тут нужно врубиться в одну вещь. Длина костей не задаётся в явном виде. В каком-то смысле костей нет вообще. Вместо этого задаются проекции смещений и поворотов между суставами. Но никто нам не мешает потом нарисовать между суставами линии, чтобы визуально увидить как выглядит скелет модели. Итак, в блоке skeleton строк столько же, сколько и в блоке nodes. Но есть ещё одна строчка, которая называется "time 0". Эта строчка означает номер кадра анимации! Обратите внимание, в файле с моделью есть один кадр анимации. А анимацию из одного кадра не сделаешь. Весь секрет в том, что это начальное положение суставов скелета, в котором записана модель. Теперь, если вы посмотрите остальные SMD-фалы (с анимацией), то в них помимо "time 0" будет ещё много кадров: "time 1", "time 2", "time 3", "time 4" и т.д.. Кадры представляют собой положение суставов скелета в пространстве. Вернёмся к разбору SMD-файла с моделью. Взглянем на структуру данных, хранящуюся в блоке skeleton под номером кадра "time 0": 0 0.254895 0.401299 43.083469
0.000000 0.000000 -1.570795 Здесь в первом столбце - номер сустава, все остальные столбцы - это "ротации" (rotation) и "трансляции" (translation). То есть здесь записаны повороты и перемещения суставов друг относительно друга. Введём соответствующие типы данных: type TPovorot=array[0..2] of single; И добавим в тип нашей модельки массивы с поворотами и перемещениями суставов: type TModelka=object Например, для нулевого сустава смысл заполнения массивов с данными будет такой: 0 0.254895 0.401299 43.083469 0.000000 0.000000 -1.570795 povorot[0][0]:=0.0; peremesh[0][0]:=0.254895; Как видно, здесь всё очень просто. Три проекции перемещения и три проекции поворота. Блоки данных после ключевых слов nodes и skeleton мы разобрали. Перемещаемся теперь дальше по SMD-файлу с моделью и находим следующий блок с данными: triangles Это блок triangles: начинается со слова triangles, и заканчивается end-ом. Из названия ясно, что это блок с треугольниками. Проще говоря это и есть сама модель. В этом блоке последовательно перечислены данные всех треугольников, из которых состоит модель. Чередование выглядит так: название текстуры, наложенной
на треугольник . bmp И так много-много раз. Вся модель записана в текстовом файле после ключевого слова triangles. Нам нужно сделать какой-нибудь тип данных, в котором мы будем хранить это огромное количество треугольников Посмотрим на первый треугольник: ammo_pack_blk.bmp Каждый треугольник состоит из трёх точек. Вначале указывается текстура, которую следует наложить на этот треугольник. В данном случае текстура называется ammo_pack_blk.bmp. Далее следует три строчки, в которых описываются вершины полигона. Теперь посмотрим на данные одной вершины: 11 -7.665082 -4.687413 65.324814 -0.292812 0.513422 0.806634 0.040323 0.250000 Здесь первое число - это номер сустава, в пространстве которого находится вершина. По-другому это можно объяснить так: 11 - это номер сустава, к которому прикреплена данная вершина. Все перемещения и повороты 11-го сустава отразятся на данной вершине. Остальные данные в этой строчке означают следующее: 11 / -7.665082 / -4.687413 / 65.324814 / -0.292812 / 0.513422 / 0.806634 / 0.040323 / 0.250000 сустав / Xвершины / Yвершины / Zвершины / Xнормали / Yнормали / Zнормали / Uтекстуры / Vтекстуры Как видно, здесь нет ничего сложного. Данные представлены в классическом виде: даны координаты вершины, вектор нормали и текстурные координаты. Единственный дополнительный параметр - это номер сустава, которому принадлежит данная вершина. Суставы ещё называют узлами скелета (nodes), а номер сустава называют "родительским" идентификатором (parent id). То есть у узлов есть "parent" узлы и у вершин (вертексов) есть "parent" узлы. Осталось создать типы данных, в котором мы будем хранить вершины и треугольники. Для начала опишем вершину: type TVershina=record Как видно, здесь есть место для всех данных вершины: sustav - номер сустава; Вернёмся к нашему формату данных: ammo_pack_blk.bmp У каждого треугольника есть название текстуры и три вершины. Введём соответствующий тип данных: type TPoligon=record Здесь tekstura - это название текстуры, а vershini - массив из трёх вершин. В итоге тип TModelka будет выглядеть так: type TModelka=object Итак, мы описали все структуры данных, необходимые для загрузки SMD-файла модели. Теперь ясно видно, что SMD-файл модели имеет следующую структуру: version 1 Напишем загрузку SMD-файлов с моделями. Так как файл текстовый, то возникает вопрос как из этих строк перевести данные в наши массивы. Удобнее всего в этом случае загружать строки этого файла по очереди, и добывать из этих строк данные с помощью готового юнита _strman.pas. Добавим этот юнит в uses модуля modelki.pas. Процедуру загрузки начнём писать с определения блоков: procedure TModelka.zagruz(fname:string); repeat repeat repeat repeat repeat repeat closeFile(f); Как видно, пока процедура ничего не загружает, а является лишь заготовкой для написания загрузчика SMD-фалов. В юните _strman есть отличная функция StringWordGet. Её описание вы можете найти в комментариях к исходникам этого юнита. Если коротко, то смысл этой функции такой: что:=StringWordGet (откуда,'разделитель',номер); Начнём с загрузки блока nodes. Нужные цифры находятся вне кавычек, а цифра слева меняется от нуля до некоторой цифры (всё время плюс один). Поэтому есть смысл выдирать только цифру, находящуюся справа от названия в кавычках: svaz[i]:=StrToInt(StringWordGet (s, '"', 3)); Теперь перейдём к загрузке блока skeleton. Здесь есть не только целые числа (integer), но и вещественные (single). Чтобы конвертировать их из текста в числа, воспользуемся процедурой StrToFloatDef из юнита _util.pas: peremesh[i][0]:=strtofloatdef(StringWordGet(trim(s),'
',2),0); И, наконец, блок triangles: poligon[i].vershini[j].sustav:=strToInt(StringWordGet(trim(s),'
',1)); Загрузчик данных из SMD-файлов с моделью полностью готов, и вызывается следующим образом: var monster:TModelka; monster.zagruz('c:\tutorial\smd\wesker.smd'); end; Теперь напишем загрузчик SMD-файлов с анимацией. Если посмотреть внутрь любого такого фала, то его структура будет выглядеть следующим образом: version1 Теперь становится ясно в чём отличие SMD-файлов с анимацией от SMD-файлов с моделью. В SMD-файлах с анимацией нет раздела triangles, в котором описывается полигональная 3d модель. Но зато там есть много кадров положения костей скелета (time 0, time1, time 2, time 3 и т.д.). А выше я уже писал, что в SMD-файлах с моделью имеется только одно положение скелета (time 0), в котором сделан mesh (полигональная модель). Написать загрузчик SMD-анимаций достаточно просто, так как выше уже были описаны нужные нам структуры данных для загрузки кадра time-0 из SMD-модели. Опишем один кадр анимации скелета: type TKadrAnimacii=record И зададим тип TAnimacia, в котором будет хранится массив кадров анимации: type TAnimacia=object Добавим к этому объекту процедуру загрузки анимации из SMD-файла: type TAnimacia=object Вернёмся к нашему загрузчику SMD-файлов с моделью и немножко изменим его, чтобы он укладывался в стиль написания этой программы. Заменим вот эти строки: type TModelka=object На следующие: type TModelka=object Это отразится в тексте программы на самом загрузчике SMD-модели. Вместо povorot и peremesh будут использоваться odinkadr.povorot и odinkadr.peremesh. Приступим непосредственно к написанию загрузчика SMD-анимации: procedure TAnimacia.zagruz(fname:string); Из файла с анимацией нас будет интересовать только раздел skeleton, в котором записаны кадры анимации: skeleton Поэтому загрузку будем делать так: считываем данные, находящиеся между строчками "time 0", "time 1", "time 2" и "time 3" т.д., где встречается слово "time". Для поиска этого слова сделаем будем использовать слудующую конструкцию: if StringWordGet (s, ' ', 1) = 'time' then Итак, мы написали загрузку всех данных из файла SMD. Теперь у нас есть два типа переменных и две процедуры для их загрузки. Начнём писать программу для проигрывания анимации. Сначала зададим переменные для 3d модели главого героя игры и для его анимации: monster:TModelka; Теперь, используя описанные выше процедуры, загрузим данные из SMD-файлов: monster.zagruz('wesker.smd'); Чтобы посмотреть, правильно ли всё загрузилось, выведем данные о загруженных моделях: listbox1.Items.Add('Количество полигонов:
'+IntToStr(length(monster.poligon))); Если посмотреть внутрь SMD файлов, то нетрудно убедиться, что количество полигонов, суставов и кадров анимации вывелось правильно. Было бы неплохо взглянуть на модель, которую мы загрузили. Воспользуемся средствами OpenGL и выведем модель без текстуры: glBegin(GL_TRIANGLES); for i:=0 to length(monster.poligon)-1
do glEnd; Теперь мы видим неподвижную модель. Приступим к самой интересной части. Нам нужно используя кадры анимации скелета заставить эту модель двигаться. Для этого понадобятся процедуры, с помощью которых можно производить перемещения и повороты точек. Воспользуемся юнитом MatrixMaths, в котором очень удобно реализована работа с матрицами. Сначала попробуем вывести сам скелет: для этого зададим тип sustavi (суставы): type sustav=record Otnosit и absolut - это матрицы суставов. Местная (относительная) и абсолютная. Tochka - это переменная, где мы будем хранить положения суставов. Так как скелет состоит из нескольких суставов, зададим массив суставов: sustavi:array of sustav; Вычислим положение суставов скелета. Для этого пройдёмся по всем узлам скелета: for i:=0 to length(monster.svaz)-1 do Сначала все суставы будут в начале координат: sustavi[i].tochka[0]:=0.0; Создаём матрицу поворота пространства суставов (у каждого сустава есть своя система координат, которая повёрнута матрицей otnosit): sustavi[i].otnosit:=Xrot(-monster.odinkadr.povorot[i][0]); С помощью CConcatMatrix мы просто перемножали матрицы. Помимо поворотов скелет описывается взаимным перемещением суставов. Поэтому получившуюся матрицу i-го сустава нужно немного изменить: sustavi[i].otnosit[3,0]:=monster.odinkadr.peremesh[i][0]; Как видно, мы сначала с помощью функций Xrot, Yrot и Zrot получили матрицы поворота относительно осей X,Y и Z. Потом мы перемножили эти матрицы процедурой CConcatMatrix. Ну а потом в получивешйся матрице i-го узла (сустава) мы поправили три элемента в соответствии с данными о перемещениях monster.odinkadr.peremesh[i]. Дальше мы смотрим, прикреплён ли сустав к другому суставу или нет: if monster.svaz[i]<>-1 then Если сустав связан с любым другим суставом (номера суставов не могут быть отрицательными), то мы попадаем в систему координат этого сустава. Другими словами можно сказать так: i-тый сустав попадает в пространство сустава monster.svaz[i]. Поэтому мы берём абсолютную матрицу "родительского" сустава и перемножаем её на относительную матрицу текущего сустава: sustavi[i].absolut:=ConcatMatrix(sustavi[monster.svaz[i]].absolut, sustavi[i].otnosit) То есть абсолютная матрица текущего сустава получена по формуле: абсолютная_матрица_сустава_i:=Перемножить( Если же номер сустава, к которому прикреплён текущей равен минус еденице: if monster.svaz[i]=-1 then то мы приравниваем абсолютную матрицу i-того сустава его относительной матрице: sustavi[i].absolut:=sustavi[i].otnosit; Таким образом, у нас в цикле происходит создание
матриц: Цель этих преобразований заключается в следующем: мы хотим получить абсолютные матрицы суставов. Почему абсолютные? Потому что перемножив точку(вектор) с координатами (0,0,0) на абсолютную матрицу сустава мы сразу получим координату этого сустава. Сначала мы воспользовались данными о связях в скелете (monster.svaz[i]) и о поворотах (monster.odinkadr.povorot[i]) и перемещениях (monster.odinkadr.peremesh[i]) i-тых суставов. Итак, что же здесь происходит. Напомню вам структуру SMD-файла: version 1 Эту структуру (nodes) мы загнали в массив monster.svaz[i]. Далее следовал такой кусок: skeleton Этот блок (skeleton) мы распихали по двум массивам: monster.odinkadr.peremesh[i] и monster.odinkadr.povorot[i]. Потом мы взяли юнит MatrixMaths.pas и воспользовались процедурами для работы с матрицами. Сначала мы сгенерировали относительные матрицы суставов (relative matrices): sustavi[i].otnosit. Потом, учитывая связи между суставами (monster.svaz[i]), мы перемножали относительные матрицы до тех пор, пока не "развернули" весь скелет. Результатом этой "развёртки" скелета являются абсолютные матрицы всех узлов (суставов): sustavi[i].absolut. Нулевой сустав не прикреплён ни к одному суставу (индекс минус один) 0 "Bip01" -1 и поэтому его относительная матрица стала абсолютной: sustavi[i].absolut:=sustavi[i].otnosit;. Выведем получившийся скелет. Для этого применим абсолютные матрицы ко всем точкам сустава: for i:=0 to length(monster.svaz)-1 do Сначала все суставы у нас находились в точке (0,0,0), так как мы их обнулили вначале цикла. Здесь же мы процедурой ApplyMatrix перемножили абсолютные матрицы i-тых суставов sustavi[i].absolut на координаты этих суставов sustavi[i].tochka. В результате мы получили ненулевые координаты sustavi[i].tochka. Вывод скелета на экран сделать совсем просто. Суставы мы выведем жирными точками белого цвета: glColor3f(1,1,1); glPointSize(3); glBegin(GL_POINTS); glEnd; Кости соединяют суставы. Для вывода костей достаточно провести линии между суставами. Зададим красный цвет и воспользуемся данными о связях между суставами, чтобы вывести кости скелета: glColor3f(1,0,0); glBegin(GL_LINES); for i:=0 to length(monster.svaz)-1 do glEnd; Статичный скелет выводится без проблем. Теперь попробуем вывести кадры анимации скелета. Сделать это окажется просто. Для этого достаточно немного видоизменить код, написанный выше. Данные о скелете мы брали из модели, т.е. из переменной monster. В этой переменной у нас есть только один кадр анимации, перемещения и повороты которого находятся в записях monster.odinkadr.peremesh[i] и monster.odinkadr.povorot[i]. Чтобы вывести кадры анимации скелета достаточно использовать вместо этих данных записи из переменной anim: sustavi[i].otnosit:=Xrot(-anim.kadri[kadr].povorot[i][0]); sustavi[i].otnosit[3,0]:=anim.kadri[kadr].peremesh[i][0]; Номер кадра мы будем периодически увеличивать на еденицу: inc(kadr); Как только номер кадра превысит общее число кадров анимации, то мы его обнулим: if kadr>length(anim.kadri)-1 then kadr:=0; Итак, теперь мы можем выводить кадры анимации. Возможно, что вас смущает одна вещь: модель почему-то куда-то уходит, а потом опять начинает свой путь из начала координат. В этом нет ничего странного: так создаются модели к Half-Life. Вы можете не учитывать peremesh[0][0] при создании абсолютной матрицы нулевого сустава, тогда модель будет стоять на месте. В своём 3d движке, который разрабатывался для создания игры в стиле Resident Evil, я использовал изменение данных из peremesh[0][0] для проецирования на наравление движения игрока. Эти данные могут понадобиться в том случае, если в модели заложено неравномерное движение (хромой зомби, который при прихрамывании резко сдвигается вперёд). Задавать такие движения вручную неудобно, для этого и существуют данные нулевого сустава. Никто не мешает нам вручную менять кадры анимации (вращать головой, руками и т.д.). Для этого мы меняем у сустава его параметры вручную тёмя скроллбарами (ползунками): for i:=0 to length(anim.kadri)-1 do Во всех кадрах анимации разом поменяли повороты десятого сустава. Здесь можно сделать эффект, аналогичный Resident Evil 3: главный герой может смотреть куда-нибудь при ходьбе. При этом нужно учесть максимально возможный угол поворота головы. Например, главный герой может поворачивать голову на важные места, как в Silent Hill 3. Теперь встаёт вопрос как применить движения скелета к самой модели. Для простоты не будем использовать кватернионы. Вообще весь смысл их использования заключается лишь в том, что можно получить промежуточные абсолютные матрицы для плавной анимации. Напомню, что треугольники в нашей модели задаются в следующем виде: triangles Для нас здесь очень важным является первое число (11). Это число показывает номер сустава, в пространстве которого находится данная вершина. И мы приходим к одному очень интересному месту. В SMD-файле с моделью была не в случайной позе. И нулевой кадр анимации для этой модели дан не случайно. Нулевой кадр анимации соответствует этой позе. И здесь в скелетной анимации применяется следующий трюк: мы как бы "сворачиваем" модель, делая скелетные преобразования наоборот. Скукоженная таким образом модель готова к "разворачиванию" любыми кадрами анимации. Сделаем массив полигонов, которые будем "сварачивать": setlength(svernuli,length(monster.poligon)); Далее пользуемся алгоритмом для создания абсолютных (глобальных) матриц скелета, но с небольшими изменениями. Эти небольшие изменения как раз служат для "сворачивания". Когда модель свернётся (все вершины её полигонов), то можно будет приступить к скелетной анимации этой модели. Я взял процедуру ApplyMatrix(Var Vert: Vertex; const M: Mat) из юнита MatrixMaths.pas и немного видоизменил её. Получилась новая процедура: Procedure InvApplyMatrix(Var Vert: Vertex; const M: Mat); А теперь более корректное объяснение. Мы применяем вместо ApplyMatrix процедуру InvApplyMatrix. Это некое "инверсное преобразование". А смысл "сворачивания" модели состоит в том, что мы определяем локальные координаты всех точек модели. Итак, для всех вершин в массиве svernuli мы выполняем следующие манипуляции: for i:=0 to length(svernuli)-1 do В результате этих манипуляций все точки в массиве svernuli становятся "локальными". Теперь к этим точкам можно применять любые глобальные матрицы анимации. Всё, что нам остаётся для анимирования модели - это выполнить "разворачивание" кадра скелета в глобальные матрицы и применение этих глобальных матриц ко всем "локализованным" точкам: for i:=0 to length(monster.poligon)-1
do for i:=0 to length(svernuli)-1 do Перед началом "разворачивания" каждого кадра анимации мы записываем рассчитанную выше "свёрнутую" модель: for i:=0 to length(monster.poligon)-1
do Свёртку модели нужно проводить только один раз. После этого они хранятся в svernuli[i]; и перед разворачиванием загоняются в monster.poligon[i]. Вспомним как мы получали изображение скелета. Сначала все суставы были в начале координат. Потом мы стали "разворачивать" скелет, получая таким образом глобальные матрицы. Потом перемножили все точки из начала координат на соответствующие им глобальные матрицы и получили коодинаты точек в глобальном пространстве. В случае с моделью приходится делать всё наоборот: по данным о глобальных матрицах мы сворачиваем все "полигоны" модели в начало координат. Модель из нормальной превращается в "скукоженную". Но это только на первый взгляд. На самом деле если вывести только те полигоны, которые принадлежан одному суставу, то этот кусочек модели будет выглядеть нормально. Короче говоря, при "сворачивании" мы повернули и переместили все части модели так, что они оказались в начале координат. А при анимации мы "разворачиваем" всё это хозяйство, основываясь на глобальных матрицах текущего кадра скелета. Я пытался описать всё как можно проще и понятнее. Изменяя и дорабатывая эти исходники вы можете написать свой загрузчик моделей Half-Life как с использованием OpenGL, так и DirectX. Также я надеюсь, что это описание поможет тем, кто хочет написать загрузчик на других языках (Visual Basic). В заключении скажу несколько слов о загрузке текстур, создании плавной анимации и прикреплении предметов к скелету. Если у вас будут какие-нибудь вопросы, пожелания или замечания, пишите мне на e-mail tmtlib@narod.ru. Загрузку текстур реализовать весьма просто. Для этого достаточно пройтись по массиву monster.poligon[i] и составить список неповторяющихся имён файлов с текстурами: перебор всех полигонов и анализ строки monster.poligon[i].tekstura. После этого загружаете текстуры какими-нибудь средствами и уже можно вывести текстурированную модель. Теперь у вас есть загруженные текстуры. При выводе j-той точки i-того полигона: monster.poligon[i].vershini[j].koordinata[0] У вас всегда есть идентификатор его текстуры monster.poligon[i].tekstura и две текстурные координаты для этой точки: monster.poligon[i].vershini[j].textkoordinata[0] Всё это было уже описано в типе "вершина": type TVershina=record Перейдём к созданию плавной анимации. Видно, что в моём примере анимация "дёргается". Сделать плавную анимацию можно двумя способами. Первый способ - это использование кватернионов (quaternion). В этом случае можно получить глобальные матрицы, плавно изменяющиеся между двумя кадрами глобальных матриц. Второй способ - это интерполяция исходных данных (povorot, peremesh). Кватернионы используются для плавной анимации вращения. Для перемещения - обычная интерполяция. Для хорошей работы с FPS (скоростью) анимации удобно использовать функцию timeGetTime из mmsystem winApi. Эта функция позволит адаптироваться к разному быстродействию компьютеров. С помощью этой функции можно очень точно измерить время между кадрами и, основываясь на этом, влиять на ход плавной интерполяции. У вас есть "ключевые" положения скелета, а ваша задача сделать промежуточные положения скелета между этими "ключевыми" положениями. Одна из интереснейших вещей в скелетной анимации - это возможность прикрепления предметов (ружья, ножи, дубинки и т.п.) к скелету. Вы также можете прикреплять части тела. Или откреплять, если, например, у зомби снесёт голову, как в Resident Evil. Кстати, мне известно, что в Resident Evil, начиная с первой части, используется скелетная анимация. Так вот, как же прикреплять эти предметы к модели? Да очень просто! Достаточно для всех вершин этого предмета применить абсолютную матрицу сустава скелета: ApplyMatrix(вершина предмета , sustavi[ номер сустава куда прикреплять ].absolut); То есть просто в цикле все вершины предмета
трансформируем с помощью абсолютной матрицы. Номер сустава - может быть
любое число. В это место прикрепится предмет. К суставу на голове можно
прикрепить солнцезащитные очки. К суставу в руке - какой-нибудь вид оружия.
Можно сделать ещё интереснее. Никто не мешает вам взять базовый (нулевой)
сустав другой склетно анимированной модели и прикрепить к суставу в другой
модели. Это реализуется простым приравниванием абсолютных матриц. Ну а
вся прелесть состоит в том, что и солнечные очки, и дубинка в руке будут
отлично двигаться вместе с вашей моделью, не смотря на свою статичность!
Да, изначально у этих объектов не было анимации, но подвергая их процедуре
ApplyMatrix мы можем анимировать их с помощью абсолютных матриц, входящих
в скелетно анимированную модель! |