Добрый день, уважаемые читатели.
Сегодня я продолжу рассказ о том, как создавать приложения при помощи 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-лицензии на полгода и год. Подробнее: конкурс.
Автор: DarkFIxED