
Добрый день, уважаемые читатели.
Сегодня я продолжу рассказ о том, как создавать приложения при помощи Swarp SDK. Этот пост является продолжением обзора Swarp SDK. В первой части мы рассмотрели пример из стандартной поставки Swarp и общие принципы работы с ним. А сейчас мы напишем свое AR-приложение, которое будет изменять анимацию 3D-модели в зависимости от наличия маркеров перед камерой. Кому интересно — прошу под кат.
О приложении
Итак, вот основные требования к нашему проекту.
В приложении будет использоваться стандартная Mogre'овская 3D-модель ниндзи (все модели и исходные коды лежат в приложении к посту). При наличии определенного маркера в поле зрения камеры необходимо отобразить модель в соответствующей ориентации. А теперь самое интересное — анимация. В модели ниндзи есть 16 (если мне не изменяет память) анимаций. Я выбрал 3 из них: Crouch, Attack1, Backflip. К каждой из этих анимаций будет привязан маркер. Если маркер анимации отсутствует на сцене, то соответствующую анимацию необходимо выполнить. Если же присутствуют все маркеры, то выполняется анимация Idle1.Вот вроде бы и все. Приступаем к реализации.
Реализация
Для создания проекта я буду использовать Visual Studio 2010. Сначала я создаю консольное приложение Win32 и называю его HabrDemo2. Кстати, для того, чтобы студия не умирала в процессе отладки приложений, необходимо включить отладку неуправляемого кода и разрешить небезопасный код во вкладках «Debug» и «Build» соответственно:


В проекте будет файл Program.cs, содержащий точку входа в приложение:

Собственно, с ним мы и будем работать.
Теперь надо подключить ссылки на используемые сборки. Мне понадобятся следующие:

Маленькая фича: Swarp SDK добавляет свои сборки на вкладку .NET добавления сборок для экономии времени разработчика:
Действительно удобно — не надо лазить по папкам в поисках установленного 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. Это квант времени, который определяет, на сколько «сдвигается» анимация в каждом кадре.
С полями закончили. На очереди — инициализация.
Инициализация приложения
Про инициализацию дополненной реальности подробнее описано в первой части, здесь я кратенько пробегусь по алгоритму инициализации и отличительным местам кода.
Сам по себе алгоритм инициализации довольно простой и состоит из следующих шагов:
- камера;
- трекер;
- рендерер.
Инициализация камеры в нашем приложении будет предельно проста и взята из стандартного примера 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-лицензии на полгода и год. Подробнее: конкурс.