netlib.narod.ru< Назад | Оглавление | Далее >

Обработчик анимированых моделей

Сейчас вам надо создать новый обработчик моделей, расширяющий используемый по умолчанию обработчик моделей XNA. Вы будете использовать этот новый обработчик для обработки анимированых моделей, извлечения их скелетов и анимаций и сохранения всего этого в объекте AnimatedModelData.

Чтобы создать новый обработчик моделей вы должны создать новый проект Content Pipeline Extension Library с именем AnimatedModelProcessorWin. Проект Content Pipeline Extension Library создается с новым классом обработчика содержимого и автоматически добавляет в проект сборку конвейера содержимого (Microsoft.Xna.Framework.Content.Pipeline). Поскольку для хранения данных анимации вы собираетесь использовать библиотеку AnimatedModelContentWin (созданную вами в предыдущем разделе), вам необходимо добавить также к проекту и ее сборку. Ниже представлен код нового класса обработчика содержимого, который создается с проектом Content Pipeline Extension:

[ContentProcessor]
public class ContentProcessor1 : ContentProcessor<TInput, TOutput>
{
    public override TOutput Process(TInput input,
                                    ContentProcessorContext context)
    {
        // TODO
        throw new NotImplementedException();
    }
}

Создаваемый по умолчанию класс обработчика содержимого расширяет класс ContentProcessor, являющийся базовым классом для любого обработчика конвейера содержимого, и используется для обработки объектов типа TInput, возвращая новый объект типа TOutput. Поскольку мы не заинтересованы в создании нового обработчика содержимого, а хотим расширить возможности существующего, надо использовать существующий обработчик содержимого вместо класса ContentProcessor. В данном случае вы расширяете класс XNA ModelProcessor, являющийся классом используемого по умолчанию обработчика моделей. Также вам следует переименовать новый класс обработчика содержимого в AnimatedModelProcessor. Вот базовая структура вашего нового обработчика моделей, класса AnimatedModelProcessor:

[ContentProcessor]
public class AnimatedModelProcessor : ModelProcessor
{
    public static string TEXTURES_PATH = "Textures/";
    public static string EFFECTS_PATH = "Effects/";
    public static string EFFECT_FILENAME = "AnimatedModel.fx";

    public override ModelContent Process(NodeContent input,
                               ContentProcessorContext context)
    {
        ...
    }

    protected override MaterialContent ConvertMaterial(
           MaterialContent material, ContentProcessorContext context)
    {
        ...
    }
}

В классе ModelProcessor есть много методов, которые можно переопределить, но вам для обработки анимированных моделей потребуется переопределить только методы Process и ConvertMaterial. Главный метод, вызываемый для обработки модели, это метод Process. Данный метод необходим для преобразования объекта NodeContent, в котором содержатся сетки, скелет и анимации модели, в объект ModelContent, хранящий данные для объекта XNA Model. Помимо метода Process, для обработки материалов модели вызывается метод ConvertMaterial.

Переопределение используемого по умолчанию метода обработки

В этом разделе вы переопределите метод Process класса ModelProcessor, вызываемый для обработки модели. Также вы создадите два новых метода для извлечения скелета и анимаций модели: ExtractSkeletonAndAnimations и ExtractAnimations, где метод ExtractAnimainons вызывается изнутри метода ExtractSkeletonAndAnimations. Вот код переопределенного метода Process:

public override ModelContent Process(NodeContent input,
                          ContentProcessorContext context)
{
    // Обрабатываем модель обработчиком по умолчанию
    ModelContent model = base.Process(input, context);

    // Извлекаем скелет модели и все ее анимации
    AnimatedModelData animatedModelData =
               ExtractSkeletonAndAnimations(input, context);

    // Сохраняем данные скелетной анимации в модели
    Dictionary<string, object> dictionary = new Dictionary<string, object>();
    dictionary.Add("AnimatedModelData", animatedModelData);
    model.Tag = dictionary;

    return model;
}

В начале метода Process вы вызываете метод Process базового класса ModelProcessor. Затем вы вызываете метод ExtractSkeletonAndAnimations, который обрабатывает исходный NodeContent и возвращает объект AnimatedModelData, содержащий скелет модели и анимации. И в конце вы создаете словарь, который связывает строки с объектами, добавляете в этот словарь AnimatedModelData и присваиваете его свойству Tag итогового объекта ModelContent. В классе XNA Model есть свойство Tag, позволяющее добавлять к модели произвольные пользовательские данные. Использование в качестве свойства Tag словаря позволяет вам добавлять к классу XNA Model много различных произвольных объектов, и запрашивать любой из них во время выполнения, используя строку.

Заметьте, что данные, которые вы устанавливаете в свойстве Tag объекта ModelContent, сохраняются вместе с данными модели в двоичном файле XNB. Эти данные будут получены, когда модель загружается с помощью диспетчера содержимого.

Извлечение скелета модели

Метод ExtractSkeletonAndAnimations получает на входе корневой объект NodeContent, который может содержать в качестве потомков объекты MeshContent и BoneContent, как было описано ранее. Для извлечения скелета модели вы должны сперва найти корневую кость скелета внутри корневого NodeContent, а затем выполнить обход вглубь скелета, создавая список костей. Класс XNA MeshHelper предоставляет ряд методов, которые помогут вам в этом процессе:

// Поиск узла корневой кости
BoneContent skeleton = MeshHelper.FindSkeleton(input);

// Преобразование иерархии в список (обход в глубину)
IList<BoneContent> boneList = MeshHelper.FlattenSkeleton(skeleton);

Вы находите корневую кость скелета, используя метод FindSkeleton класса MeshHelper. Затем вам необходимо преобразовать дерево скелета в список, используя поиск в глубину. Делается это с помощью метода FlattenSkeleton класса MeshHelper. Результатом является список костей, где каждая кость это объект класса BoneContent. Заметьте, что кости в этом списке идут в том же самом порядке, в котором они индексируются по вершинам сетки.

Для каждой кости в созданном списке вы должны сохранить ее локальную конфигурацию в позиции привязки, ее инвертированную абсолютную конфигурацию в позиции привязки и индекс ее родителя. Вы читаете локальную и абсолютную конфигурацию кости из свойств Transform и AbsoluteTransform объекта BoneContent, и вычисляете инвертированную абсолютную конфигурацию кости с помощью метода Invert класса XNA Matrix:

bonesBindPose[i] = boneList[i].Transform;
bonesInverseBindPose[i] = Matrix.Invert(boneList[i].AbsoluteTransform);

Вот полный код метода ExtractSkeletonAndAnimations:

private AnimatedModelData ExtractSkeletonAndAnimations(NodeContent input,
                    ContentProcessorContext context)
{
    // Находим узел корневой кости
    BoneContent skeleton = MeshHelper.FindSkeleton(input);

    // Преобразуем иерархию в список (обход в глубину)
    IList<BoneContent> boneList =
              MeshHelper.FlattenSkeleton(skeleton);
    context.Logger.LogImportantMessage("{0} bones found.",
              boneList.Count);

    // Создаем позицию привязки скелета, инвертированную позицию привязки
    // и массив родителей 
    Matrix[] bonesBindPose = new Matrix[boneList.Count];
    Matrix[] bonesInverseBindPose = new Matrix[boneList.Count];
    int[] bonesParentIndex = new int[boneList.Count];
    List<string> boneNameList = new List<string>(boneList.Count);

    // Извлекаем и сохраняем необходимые данные из списка костей
    for (int i = 0; i < boneList.Count; i++)
    {
        bonesBindPose[i] = boneList[i].Transform;
        bonesInverseBindPose[i] =
                Matrix.Invert(boneList[i].AbsoluteTransform);
        int parentIndex =
                boneNameList.IndexOf(boneList[i].Parent.Name);
        bonesParentIndex[i] = parentIndex;
        boneNameList.Add(boneList[i].Name);
    }

    // Извлекаем все анимации
    AnimationData[] animations = ExtractAnimations(
                skeleton.Animations, boneNameList, context);

    return new AnimatedModelData(bonesBindPose, bonesInverseBindPose,
                                 bonesParentIndex, animations);
}

После извлечения скелета модели вы должны вызвать метод ExtractAnimations для извлечения анимаций модели.

Извлечение анимации модели

Все анимации модели хранятся как словарь анимаций, сопоставляющий строку, содержащую имя анимации, с объектом AnimationContent, хранящим данные анимации. Вы получаете доступ к словарю анимаций через свойство Animations корневого узла типа BoneContent скелета модели. Заметьте, что у конвейера содержимого есть его собственные классы для хранения данных анимации модели: AnimationContent, AnimationChannel и AnimationKeyframe. Класс AnimationContent хранит законченную анимацию модели в виде массива объектов AnimationChannel, где каждый объект AnimationChannel хранит анимацию отдельной кости в виде массива объектов AnimationKeyframe. Итак, класс XNA AnimationContent хранит анимацию для каждой кости отдельно, в то время как вы храните их вместе в одном массиве.

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

private AnimationData[] ExtractAnimations(
                   AnimationContentDictionary animationDictionary,
                   List<string> boneNameList,
                   ContentProcessorContext context)
{
    context.Logger.LogImportantMessage("{0} animations found.",
                                       animationDictionary.Count);

    AnimationData[] animations = new
                         AnimationData[animationDictionary.Count];
    int count = 0;

    foreach (AnimationContent animationContent in animationDictionary.Values)
    {
        // Сохраняем все ключевые кадры анимации
        List<Keyframe> keyframes = new List();

        // Перебираем все каналы анимации
        // У каждой кости есть ее собственный канал
        foreach (string animationKey in animationContent.Channels.Keys)
        {
            AnimationChannel animationChannel =
                              animationContent.Channels[animationKey];
            int boneIndex = boneNameList.IndexOf(animationKey);
            foreach (AnimationKeyframe keyframe in animationChannel)
                keyframes.Add(new Keyframe(
                         keyframe.Time, boneIndex, keyframe.Transform));
        }

        // Сортируем все кадры анимации по времени
        keyframes.Sort();
        animations[count++] = new AnimationData(animationContent.Name,
                        animationContent.Duration, keyframes.ToArray());
    }

    return animations;
}

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

Сейчас у вас есть скелет и анимации модели извлеченные и сохраненные в удобном формате, готовом к записи в двоичный файл XNB.

ПРИМЕЧАНИЕ
За дополнительной информацией об обобщенном классе List и интерфейсе IComparable, обращайтесь к справочным файлам C#, поскольку они предоставляются .NET Framework, а не XNA.

Чтение и запись произвольных пользовательских данных

Созданный вами AnimatedModelProcessor хранит данные скелетной анимации модели, используя несколько нестандартных пользовательских объектов (классы AnimatedModelData, AnimationData и Keyframe). Конвейеру содержимого надо читать и записывать эти объекты в двоичный файл, но конвейер содержимого не знает как читать или записывать ваши нестандартные объекты.

Чтобы определить, как данные скелетной анимации должны читаться и записываться в двоичном файле, вы должны создать записыватель содержимого и считыватель содержимого для каждого класса созданного вами для хранения данных скелетной анимации. В данном случае вам надо создать новый считыватель содержимого и новый записыватель содержимого для классов AnimatedModelData, AnimationData и Keyframe. Вы создаете считыватели и записыватели содержимого расширяя классы XNA ContentTypeReader и ContentTypeWriter.

Записыватель содержимого

Чтобы создать записыватель содержимого вы должны добавить к проекту AnimatedModelProcessorWin новый элемент Content Type Writer с именем AnimatedModelDataWriter. Классы записывателей содержимого надо просто добавить к проекту обработчика моделей. В файл записывателя содержимого вы добавляете три новых класса, KeyframeWriter, AnimationDataWriter и AnimatedModelDataWriter, которые используются для записи данных классов Keyframe, AnimationData и AnimatedModelData. Каждый из этих трех классов расширяет класс ContentTypeWriter и переопределяет его метод Write.

Метод Write класса ContentTypeWriter получает два параметра. Первый из них ContentWriter, используемый для записи данных объекта в двоичный файл, а второй — записываемый объект. Внутри метода Write вы должны записать все атрибуты класса, используя объект ContentWriter. Обратите внимание на важность порядка, в котором объекты записываются в двоичный файл, он должен совпадать с порядком в котором они будут считываться. Вот код классов KeyframeWriter, AnimationDataWriter и AnimatedModelDataWriter:

[ContentTypeWriter]
public class KeyframeWriter : ContentTypeWriter<Keyframe>
{
    protected override void Write(ContentWriter output, Keyframe value)
    {
        output.WriteObject(value.Time);
        output.Write(value.Bone);
        output.Write(value.Transform);
    }

    public override string GetRuntimeReader(TargetPlatform targetPlatform)
    {
        return typeof(KeyframeReader).AssemblyQualifiedName;
    }
}

[ContentTypeWriter]
public class AnimationDataWriter : ContentTypeWriter<AnimationData>
{
    protected override void Write(ContentWriter output, AnimationData value)
    {
        output.Write(value.Name);
        output.WriteObject(value.Duration);
        output.WriteObject(value.Keyframes);
    }

    public override string GetRuntimeReader(TargetPlatform targetPlatform)
    {
        return typeof(AnimationDataReader).AssemblyQualifiedName;
    }
}

[ContentTypeWriter]
public class AnimatedModelDataWriter : ContentTypeWriter<AnimatedModelData>
{
    protected override void Write(ContentWriter output, AnimatedModelData value)
    {
        output.WriteObject(value.BonesBindPose);
        output.WriteObject(value.BonesInverseBindPose);
        output.WriteObject(value.BonesParent);
        output.WriteObject(value.Animations);
    }

    public override string GetRuntimeReader(TargetPlatform targetPlatform)
    {
        return typeof(AnimatedModelDataReader).AssemblyQualifiedName;
    }
}

Считыватель содержимого

Чтобы создать считыватель содержимого вы должны добавить к проекту AnimatedModelContentWin новый элемент Content Type Reader с именем AnimatedModelDataReader. Игровому приложению нужны классы считывателей содержимого, а не классы записывателей, чтобы загрузить данные анимации во время исполнения.

Вам надо создать три новых класса, KeyframeReader, AnimationDataReader и AnimatedModelDataReader, которые будут использоваться для чтения данных классов Keyframe, AnimationData и AnimatedModelData. Каждый из этих классов должен расширять класс ContentTypeReader и переопределять его метод Read.

Метод Read класса ContentTypeReader получает два параметра. Первый из них, это ContentReader, используемый для чтения данных объекта из двоичного файла, а второй параметр — это ссылка на существующий экземпляр объекта. Второй параметр всегда будет null, поскольку вы создаете объект. И снова обратите внимание на то, что объекты внутри метода Read должны считываться в том же самом порядке, в котором они записывались. Вот код классов KeyframeReader, AnimationDataReader и AnimatedModelDataReader:

public class KeyframeReader : ContentTypeReader<Keyframe>
{
    protected override Keyframe Read(ContentReader input,
                                     Keyframe existingInstance)
    {
        TimeSpan time = input.ReadObject<TimeSpan>();
        int boneIndex = input.ReadInt32();
        Matrix transform = input.ReadMatrix();

        return new Keyframe(time, boneIndex, transform);
    }
}

public class AnimationDataReader : ContentTypeReader<AnimationData>
{
    protected override AnimationData Read(ContentReader input,
                                 AnimationData existingInstance)
    {
        string name = input.ReadString();
        TimeSpan duration = input.ReadObject<TimeSpan>();
        Keyframe[] keyframes = input.ReadObject<Keyframe[]>();

        return new AnimationData(name, duration, keyframes);
    }
}

public class AnimatedModelDataReader : ContentTypeReader<AnimatedModelData>
{
    protected override AnimatedModelData Read(ContentReader input,
                                   AnimatedModelData existingInstance)
    {
        Matrix[] bonesBindPose = input.ReadObject<Matrix[]>();
        Matrix[] bonesInverseBindPose = input.ReadObject<Matrix[]>();
        int[] bonesParent = input.ReadObject<int[]>();
        AnimationData[] animations =
                    input.ReadObject<AnimationData[]>();

        return new AnimatedModelData(bonesBindPose,
                      bonesInverseBindPose, bonesParent, animations);
    }
}

netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz