Swarp SDK или о том, как быстро создать AR-приложение. Часть 2

в 6:42, , рубрики: .net, SDK, дополненная реальность, метки: , ,

Swarp SDK или о том, как быстро создать AR приложение. Часть 2

Добрый день, уважаемые читатели.

Сегодня я продолжу рассказ о том, как создавать приложения при помощи Swarp SDK. Этот пост является продолжением обзора Swarp SDK. В первой части мы рассмотрели пример из стандартной поставки Swarp и общие принципы работы с ним. А сейчас мы напишем свое AR-приложение, которое будет изменять анимацию 3D-модели в зависимости от наличия маркеров перед камерой. Кому интересно — прошу под кат.

О приложении

Итак, вот основные требования к нашему проекту.
В приложении будет использоваться стандартная Mogre'овская 3D-модель ниндзи (все модели и исходные коды лежат в приложении к посту). При наличии определенного маркера в поле зрения камеры необходимо отобразить модель в соответствующей ориентации. А теперь самое интересное — анимация. В модели ниндзи есть 16 (если мне не изменяет память) анимаций. Я выбрал 3 из них: Crouch, Attack1, Backflip. К каждой из этих анимаций будет привязан маркер. Если маркер анимации отсутствует на сцене, то соответствующую анимацию необходимо выполнить. Если же присутствуют все маркеры, то выполняется анимация Idle1.Вот вроде бы и все. Приступаем к реализации.

Реализация

Для создания проекта я буду использовать Visual Studio 2010. Сначала я создаю консольное приложение Win32 и называю его HabrDemo2. Кстати, для того, чтобы студия не умирала в процессе отладки приложений, необходимо включить отладку неуправляемого кода и разрешить небезопасный код во вкладках «Debug» и «Build» соответственно:
Swarp SDK или о том, как быстро создать AR приложение. Часть 2

Swarp SDK или о том, как быстро создать AR приложение. Часть 2

В проекте будет файл Program.cs, содержащий точку входа в приложение:
Swarp SDK или о том, как быстро создать AR приложение. Часть 2
Собственно, с ним мы и будем работать.

Теперь надо подключить ссылки на используемые сборки. Мне понадобятся следующие:
Swarp SDK или о том, как быстро создать AR приложение. Часть 2
Маленькая фича: Swarp SDK добавляет свои сборки на вкладку .NET добавления сборок для экономии времени разработчика:
Swarp SDK или о том, как быстро создать AR приложение. Часть 2
Действительно удобно — не надо лазить по папкам в поисках установленного SDK.

На этом подготовка окончена — начинаем писать программу.

Пишем код

Поля

Нам понадобится несколько приватных полей:

/// <summary>
/// Менеджер дополненной реальности.
/// </summary>
private static AugmentedRealityManager _arManager;

/// <summary>
/// Объект отслеживания.
/// </summary>
private static ITrackable _ninjaTrackable;

/// <summary>
/// Маркеры активации анимации.
/// </summary>
private static List<ITrackable> _animationTrackables=new List<ITrackable>();


/// <summary>
/// Cостояние анимации.
/// </summary>
private static AnimationState _animationState;

/// <summary>
/// Cдвиг анимации.
/// </summary>
private static float _animTime = 0.1f;

Поле _arManager — экземпляр основного класса Swarp, который осуществляет процесс дополненной реальности. Вызывая его методы Start() и Stop(), мы запускаем или останавливаем процесс. Интерфейс ITrackable описывает маркеры, используемые в Swarp. Я использую экземпляр _ninjaTrackable для описания маркера, который отвечает за отображение 3D-модели и список _animationTrackables, хранящий в себе информацию о маркерах анимации. Еще я использую поле _animationState. Оно необходимо для того, чтобы описать анимацию модели. Класс AnimationState описан в сборке Mogre. Ну и последнее поле — _animTime. Это квант времени, который определяет, на сколько «сдвигается» анимация в каждом кадре.

С полями закончили. На очереди — инициализация.

Инициализация приложения

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

Сам по себе алгоритм инициализации довольно простой и состоит из следующих шагов:

  1. камера;
  2. трекер;
  3. рендерер.

Инициализация камеры в нашем приложении будет предельно проста и взята из стандартного примера Swarp SDK «OgreSimpleSceneExample»:

//используем первую попавшуюся камеру
var camera = CameraManager.Instance.GetFirstWorkedCamera();
if (camera == null)
{
    MessageBox.Show(Resources.CameraNotFoundString, Resources.WarningString,
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
    return;
}
//если 640х480х30FPS не поддерживается, то используем первые попавшиеся параметры
var displayParameters = new DisplayParameters(30, new Size(640, 480));
camera.DisplayParameters = camera.IsSupportedDisplayParameters(displayParameters)
                                      ? displayParameters
                                      : camera.GetSupportedDisplayParameters()[0];

Особого внимания заслуживает трекер.
Здесь я инициализирую трекер и загружаю используемые маркеры:

//будем использовать трекер квадратных маркеров,
//максимальное количество одновременно обнаруживаемых маркеров - 4
var tracker = new SquareMarkerTracker(4);

//загружаем сами маркеры
_ninjaTrackable = new SquareMarker("Ninja Marker", "Trackable\1.trackable");
var animationAttackTrackable = new SquareMarker("Attack3", "Trackable\5.trackable");
var animationCrouchTrackable = new SquareMarker("Crouch", "Trackable\6.trackable");
var animationBackflipTrackable = new SquareMarker("Backflip", "Trackable\7.trackable");
_animationTrackables.Add(animationCrouchTrackable);
_animationTrackables.Add(animationBackflipTrackable);
_animationTrackables.Add(animationAttackTrackable);
   
//и добавляем их в трекер
tracker.TrackableObjects.Add(_ninjaTrackable);
tracker.TrackableObjects.AddRange(_animationTrackables);

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

А теперь инициализация рендерера:

//создаем окно, в котором будет отображаться дополненная реальность
var ogreSimpleSceneForm = new Form
                              {
                                  Size = new Size(640, 480),
                                  Text = Resources.SwarpSDKExampleString,
                              };

//инициализируем рендерер
var renderer = new Ogre3DRenderSystem(ogreSimpleSceneForm.Handle, 
    "OgreConfig\plugins.cfg", "OgreConfig\ogre.cfg", "OgreConfig\ogre.log")
                   {
                       SizeMode = SizeMode.Autosize
                   };

//загружаем ресурсы с файла конфигурации
renderer.LoadResources("OgreConfig\resources.cfg");

//создаем сцену. Загружаем модель.
var ninjaScene = new Ogre3DScene(renderer.Root, "Ninja Scene");
renderer.Scenes.Add(ninjaScene);

LoadScene(ninjaScene.SceneManager);

Вроде бы все понятно, но в самом конце притаился метод LoadScene(). В его задачу входит загрузка модели, добавление ее к сцене, натягивание текстуры и запуск анимации Idle:

private static void LoadScene(SceneManager sceneManager)
{
    // Создаем узел, относительно которого будет отображаться модель.
    var ninjaNode = sceneManager.RootSceneNode.CreateChildSceneNode("NinjaNode");

    // Создаем сущность модели (загружаем ее из файла).
    var ninjaEntity = sceneManager.CreateEntity("Ninja", "ninja.mesh");
    _animationState = ninjaEntity.GetAnimationState("Idle1");
    _animationState.Loop = true;
    _animationState.Enabled = true;
  
    //натягиваем текстуру на модель
    MaterialPtr material = MaterialManager.Singleton.Create("NinjaMaterial",
                                                            ResourceGroupManager.DEFAULT_RESOURCE_GROUP_NAME);

    material.GetTechnique(0).GetPass(0).CreateTextureUnitState("nskingr.jpg");
    material.GetTechnique(0).GetPass(0).SetSceneBlending(SceneBlendType.SBT_TRANSPARENT_ALPHA);
    material.GetTechnique(0).GetPass(0).DepthCheckEnabled = true;
    material.GetTechnique(0).GetPass(0).DepthWriteEnabled = true;
    material.GetTechnique(0).GetPass(0).LightingEnabled = false;
    ninjaEntity.SetMaterial(material);
    //делаем модельку больше в 3 раза, чтобы удобно было рассматривать
    ninjaNode.Scale(3f,3f,3f);
    ninjaNode.AttachObject(ninjaEntity);
    ninjaNode.SetVisible(true);
}

Теперь все инициализировано.

Основной цикл программы

Теперь самое время запустить камеру, запустить менеджер дополненной реальности и наслаждаться процессом. Возникает вопрос — «как?». Те, кто внимательно прочитал первую статью скорее всего уже знают ответ: недаром у нас есть поле _arManager и переменная camera.

// Инициализируем менеджер дополненной реальности.
_arManager = new AugmentedRealityManager(camera, tracker, renderer);

// Запускаем камеру.
camera.Start();

// Запускаем процесс дополненной реальности.
_arManager.Start();

ogreSimpleSceneForm.Show();

while (ogreSimpleSceneForm.Created)
{
    // Обновляем положение сцен связанных с найденными в кадре объектами,
    UpdateScenes(ninjaScene.SceneManager);

    // Отрисовываем кадр вручную. 
    // Важно, чтобы кадр отрисовывался в том же потоке, 
    // в котором происходила инициализация окна рендеринга.
    _arManager.Renderer.RenderFrame();

    // Вызываем обработку всех событий в приложении.
    Application.DoEvents();
}

// Перед закрытием окна приложения необходимо очистить ресурсы.
_arManager.Dispose();
renderer.Dispose();
camera.Dispose();

В этом участке кода выполняется детектирование маркеров, активация соответствующей анимации и отрисовка кадра, пока существует форма рендеринга. Вроде тоже никаких проблем. Переходим к «сердцу» программы: методу UpdateScenes.

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

private static void UpdateScenes(SceneManager ninjaSceneManager)
{
    foreach (var tracker in _arManager.Trackers)
    {
        // Копируем список с найденными значения, 
        // так как пока идет рендеринг, эта коллекция может измениться в другом потоке.
        var foundTrackableObjects = new List<ITrackable>(tracker.LastDetectedObjects);

        // Для каждой сцены сравниваем объект отслеживания.
        foreach (var scene in _arManager.Renderer.Scenes)
        {
            // Если его нет среди найденный объектов отслеживания, то скрыть сцену.
            if (foundTrackableObjects.Contains(_ninjaTrackable))
            {
                // Если нашли объект отслеживания, связанный с данной сценой.
                scene.Visible = true;

                // Находим положение объекта отслеживания.
                var trackablePose = tracker.GetPose(_ninjaTrackable);

                // Поворачиваем сцену согласно новой позицией объекта отслеживания.
                scene.OrientScene(trackablePose);

                 //получим сущность ниндзи (классно звучит =))
                 var ninjaEntity = ninjaSceneManager.GetEntity("Ninja");
                
                 //получим нужную анимацию
                 var newAnimationState =
                     ninjaEntity.GetAnimationState(SelectAnimation(foundTrackableObjects, _animationTrackables));
                 //если нужно изменить анимацию, то настроим ее
                 if (newAnimationState.AnimationName != _animationState.AnimationName)
                 {
                     _animationState.Enabled = false;
                     _animationState = newAnimationState;
                     _animationState.Loop = true;
                     _animationState.Enabled = true;
                 }
                //а если нет, то просто добавим квант времени к анимации
                _animationState.AddTime(_animTime);                
            }
            else
            {
                scene.Visible = false;
            } 
        }
    }
}

private static string SelectAnimation(ICollection<ITrackable> found, IEnumerable<ITrackable> target)
{
    foreach (var trackable in target.Where(trackable => !found.Contains(trackable)))
    {
        return trackable.Name;
    }
    return "Idle1";
}

Обратите внимание на метод SelectAnimation. Он имеет 2 параметра: список найденных маркеров и список зарегистрированных маркеров анимации. Этот метод возвращает имя первого не найденного маркера анимации. В случае, когда все маркеры анимации присутствуют, от возвращает строку «Idle1». Именно для этого я называл маркеры именем связанной с ним анимации.

Подводный камень: обратите внимание на этот участок кода:

//получим нужную анимацию
var newAnimationState =
   ninjaEntity.GetAnimationState(SelectAnimation(foundTrackableObjects, _animationTrackables));
//если нужно изменить анимацию, то настроим ее
if (newAnimationState.AnimationName != _animationState.AnimationName)
{
    _animationState.Enabled = false;
    _animationState = newAnimationState;
    _animationState.Loop = true;
    _animationState.Enabled = true;
}

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

Ну что, самое время посмотреть, что у нас получилось?

Результаты

Ниже представлено видео, которое я записал. Мне кажется, что оно красноречивее любого текста покажет что у нас получилось.

Довольно таки забавно. Кстати говоря, Swarp детектирует маркер, даже если он частично перекрыт.

Ссылки

Исходники: HabrDemo2.rar
Сайт разработчиков: www.sectar.com
Ссылка на скачивание SDK: Swarp SDK
Managed Ogre: Mogre wiki

UPD: Кстати, буквально вчера, Сектар объявил конкурс. За участие можно получить халявный ключ до 31 августа, а победителям дают нахаляву development-лицензии на полгода и год. Подробнее: конкурс.

Автор: DarkFIxED

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js