Поделиться через


ARKit 2 в Xamarin.iOS

ARKit значительно зрел с момента его внедрения в прошлом году в iOS 11. В первую очередь, теперь можно обнаружить вертикальные, а также горизонтальные плоскости, что значительно повышает практическисть возможностей дополненной реальности в помещении. Кроме того, существуют новые возможности:

  • Распознавание эталонных изображений и объектов в качестве соединения между реальным миром и цифровыми изображениями
  • Новый режим освещения, имитирующий реальное освещение
  • Возможность совместного использования и сохранения сред AR
  • Новый формат файла, предпочтительный для хранения содержимого AR

Распознавание ссылочных объектов

Одна из демонстраций функций в ARKit 2 — это возможность распознавать эталонные изображения и объекты. Эталонные изображения можно загрузить из обычных файлов изображений (обсуждалось позже), но эталонные объекты должны быть проверены с помощью ориентированного ARObjectScanningConfigurationна разработчика.

Пример приложения: сканирование и обнаружение трехмерных объектов

Пример — это порт проекта Apple, демонстрирующего следующее:

  • Управление состоянием приложения с помощью NSNotification объектов
  • Настраиваемая визуализация
  • Сложные жесты
  • Сканирование объектов
  • Хранение ARReferenceObject

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

Управление состоянием с помощью объектов NSNotification

В этом приложении используется компьютер состояния, который переходит между следующими состояниями:

  • AppState.StartARSession
  • AppState.NotReady
  • AppState.Scanning
  • AppState.Testing

Кроме того, используется внедренный набор состояний и переходов при выполнении AppState.Scanning:

  • Scan.ScanState.Ready
  • Scan.ScanState.DefineBoundingBox
  • Scan.ScanState.Scanning
  • Scan.ScanState.AdjustingOrigin

Приложение использует реактивную архитектуру, которая публикует уведомления NSNotificationCenter о переходе состояния и подписывается на эти уведомления. Программа установки выглядит следующим образом ViewController.cs:

// Configure notifications for application state changes
var notificationCenter = NSNotificationCenter.DefaultCenter;

notificationCenter.AddObserver(Scan.ScanningStateChangedNotificationName, State.ScanningStateChanged);
notificationCenter.AddObserver(ScannedObject.GhostBoundingBoxCreatedNotificationName, State.GhostBoundingBoxWasCreated);
notificationCenter.AddObserver(ScannedObject.GhostBoundingBoxRemovedNotificationName, State.GhostBoundingBoxWasRemoved);
notificationCenter.AddObserver(ScannedObject.BoundingBoxCreatedNotificationName, State.BoundingBoxWasCreated);
notificationCenter.AddObserver(BoundingBox.ScanPercentageChangedNotificationName, ScanPercentageChanged);
notificationCenter.AddObserver(BoundingBox.ExtentChangedNotificationName, BoundingBoxExtentChanged);
notificationCenter.AddObserver(BoundingBox.PositionChangedNotificationName, BoundingBoxPositionChanged);
notificationCenter.AddObserver(ObjectOrigin.PositionChangedNotificationName, ObjectOriginPositionChanged);
notificationCenter.AddObserver(NSProcessInfo.PowerStateDidChangeNotification, DisplayWarningIfInLowPowerMode);

Типичный обработчик уведомлений обновит пользовательский интерфейс и, возможно, изменит состояние приложения, например этот обработчик, который обновляется по мере сканирования объекта:

private void ScanPercentageChanged(NSNotification notification)
{
    var pctNum = TryGet<NSNumber>(notification.UserInfo, BoundingBox.ScanPercentageUserKey);
    if (pctNum == null)
    {
        return;
    }
    double percentage = pctNum.DoubleValue;
    // Switch to the next state if scan is complete
    if (percentage >= 100.0)
    {
        State.SwitchToNextState();
    }
    else
    {
        DispatchQueue.MainQueue.DispatchAsync(() => navigationBarController.SetNavigationBarTitle($"Scan ({percentage})"));
    }
}

Наконец, Enter{State} методы изменяют модель и ПОЛЬЗОВАТЕЛЬСКИЙ интерфейс в соответствии с новым состоянием:

internal void EnterStateTesting()
{
    navigationBarController.SetNavigationBarTitle("Testing");
    navigationBarController.ShowBackButton(false);
    loadModelButton.Hidden = true;
    flashlightButton.Hidden = false;
    nextButton.Enabled = true;
    nextButton.SetTitle("Share", UIControlState.Normal);

    testRun = new TestRun(sessionInfo, sceneView);
    TestObjectDetection();
    CancelMaxScanTimeTimer();
}

Настраиваемая визуализация

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

Это облачная точка доступна разработчикам в свойстве ARFrame.RawFeaturePoints . Визуализация облака точки эффективно может быть сложной проблемой. Перебор по точкам, а затем создание и размещение нового узла SceneKit для каждой точки убьет частоту кадров. Кроме того, если это делается асинхронно, будет задержка. Пример поддерживает производительность с помощью трех частей стратегии:

  • Использование небезопасного кода для закрепления данных на месте и интерпретации данных как необработанного буфера байтов.
  • Преобразование этого необработанного буфера в SCNGeometrySource объект и создание объекта "template". SCNGeometryElement
  • Быстрое "стежка вместе" необработанных данных и шаблона с помощью SCNGeometry.Create(SCNGeometrySource[], SCNGeometryElement[])
internal static SCNGeometry CreateVisualization(NVector3[] points, UIColor color, float size)
{
  if (points.Length == 0)
  {
    return null;
  }

  unsafe
  {
    var stride = sizeof(float) * 3;

    // Pin the data down so that it doesn't move
    fixed (NVector3* pPoints = &amp;points[0])
    {
      // Important: Don't unpin until after `SCNGeometry.Create`, because geometry creation is lazy

      // Grab a pointer to the data and treat it as a byte buffer of the appropriate length
      var intPtr = new IntPtr(pPoints);
      var pointData = NSData.FromBytes(intPtr, (System.nuint) (stride * points.Length));

      // Create a geometry source (factory) configured properly for the data (3 vertices)
      var source = SCNGeometrySource.FromData(
        pointData,
        SCNGeometrySourceSemantics.Vertex,
        points.Length,
        true,
        3,
        sizeof(float),
        0,
        stride
      );

      // Create geometry element
      // The null and bytesPerElement = 0 look odd, but this is just a template object
      var template = SCNGeometryElement.FromData(null, SCNGeometryPrimitiveType.Point, points.Length, 0);
      template.PointSize = 0.001F;
      template.MinimumPointScreenSpaceRadius = size;
      template.MaximumPointScreenSpaceRadius = size;

      // Stitch the data (source) together with the template to create the new object
      var pointsGeometry = SCNGeometry.Create(new[] { source }, new[] { template });
      pointsGeometry.Materials = new[] { Utilities.Material(color) };
      return pointsGeometry;
    }
  }
}

Результат выглядит следующим образом.

point_cloud

Сложные жесты

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

Во-первых, все распознаватели жестов активируются только после прохождения порогового значения; Например, палец перетаскивал столько пикселей или поворот превышает какой-то угол. Метод заключается в том, чтобы накапливать перемещение до превышения порогового значения, а затем применить его постепенно:

// A custom rotation gesture recognizer that fires only when a threshold is passed
internal partial class ThresholdRotationGestureRecognizer : UIRotationGestureRecognizer
{
    // The threshold after which this gesture is detected.
    const double threshold = Math.PI / 15; // (12°)

    // Indicates whether the currently active gesture has exceeded the threshold
    private bool thresholdExceeded = false;

    private double previousRotation = 0;
    internal double RotationDelta { get; private set; }

    internal ThresholdRotationGestureRecognizer(IntPtr handle) : base(handle)
    {
    }

    // Observe when the gesture's state changes to reset the threshold
    public override UIGestureRecognizerState State
    {
        get => base.State;
        set
        {
            base.State = value;

            switch(value)
            {
                case UIGestureRecognizerState.Began :
                case UIGestureRecognizerState.Changed :
                    break;
                default :
                    // Reset threshold check
                    thresholdExceeded = false;
                    previousRotation = 0;
                    RotationDelta = 0;
                    break;
            }
        }
    }

    public override void TouchesMoved(NSSet touches, UIEvent evt)
    {
        base.TouchesMoved(touches, evt);

        if (thresholdExceeded)
        {
            RotationDelta = Rotation - previousRotation;
            previousRotation = Rotation;
        }

        if (! thresholdExceeded && Math.Abs(Rotation) > threshold)
        {
            thresholdExceeded = true;
            previousRotation = Rotation;
        }
    }
}

Вторая интересная вещь, выполняемая в связи с жестами, заключается в том, как ограничивающий прямоугольник перемещается в отношении обнаруженных реальных самолетов. Этот аспект рассматривается в этой записи блога Xamarin.

Другие новые возможности в ARKit 2

Дополнительные конфигурации отслеживания

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

AROrientationTrackingConfiguration, рассмотренный в этой записи блога и примере F#, является самым ограниченным и обеспечивает плохой опыт смешанной реальности, так как он только помещает цифровые объекты в связи с движением устройства, не пытаясь связать устройство и экран в реальном мире.

Это ARImageTrackingConfiguration позволяет распознавать реальные 2D-изображения (картины, логотипы и т. д.) и использовать их для привязки цифровых изображений:

var imagesAndWidths = new[] {
    ("cover1.jpg", 0.185F),
    ("cover2.jpg", 0.185F),
     //...etc...
    ("cover100.jpg", 0.185F),
};

var referenceImages = new NSSet<ARReferenceImage>(
    imagesAndWidths.Select( imageAndWidth =>
    {
      // Tuples cannot be destructured in lambda arguments
        var (image, width) = imageAndWidth;
        // Read the image
        var img = UIImage.FromFile(image).CGImage;
        return new ARReferenceImage(img, ImageIO.CGImagePropertyOrientation.Up, width);
    }).ToArray());

configuration.TrackingImages = referenceImages;

Существует два интересных аспекта этой конфигурации:

  • Это эффективно и может использоваться с потенциально большим количеством эталонных изображений
  • Цифровое изображение привязано к изображению, даже если это изображение перемещается в реальном мире (например, если обложка книги распознается, она будет отслеживать книгу, как она вытащила полку, лежала и т. д.).

ARObjectScanningConfiguration Ранее обсуждалась и является конфигурацией, ориентированной на разработчика, для сканирования трехмерных объектов. Он является высокопроцессорным и зарядным зарядом и не должен использоваться в приложениях конечных пользователей.

Окончательная конфигурация ARWorldTrackingConfiguration отслеживания — это рабочая лошадка большинства возможностей смешанной реальности. Эта конфигурация использует "визуальную инерционную odometry" для связи реальных "точек признаков" с цифровыми изображениями. Цифровые геометрии или спрайты привязаны относительно реальных и вертикальных плоскостей или относительно обнаруженных ARReferenceObject экземпляров. В этой конфигурации источник мира является исходной позицией камеры в пространстве с оси Z, выровненной по тяжести, и цифровые объекты "остаются на месте" относительно объектов в реальном мире.

Распознавание окружающей среды

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

демонстрационный образ для анализа окружающей среды

Чтобы использовать экологические тексты, выполните приведенные далее действия.

var sphere = SCNSphere.Create(0.33F);
sphere.FirstMaterial.LightingModelName = SCNLightingModel.PhysicallyBased;
// Shiny metallic sphere
sphere.FirstMaterial.Metalness.Contents = new NSNumber(1.0F);
sphere.FirstMaterial.Roughness.Contents = new NSNumber(0.0F);

// Session configuration:
var configuration = new ARWorldTrackingConfiguration
{
    PlaneDetection = ARPlaneDetection.Horizontal | ARPlaneDetection.Vertical,
    LightEstimationEnabled = true,
    EnvironmentTexturing = AREnvironmentTexturing.Automatic
};

Хотя идеально отраженная текстура, показанная в приведенном выше фрагменте кода, весело в примере, экологические текстуры, вероятно, лучше использовать с сдержанностью, чтобы она вызвала ответ "uncanny valley" (текстура является только оценкой на основе того, что камера записана).

Общие и постоянные интерфейсы AR

Еще одним основным дополнением ARWorldMap к ARKit 2 является класс, который позволяет совместно использовать или хранить данные отслеживания мира. Вы получаете текущую карту мира с ARSession.GetCurrentWorldMapAsync или GetCurrentWorldMap(Action<ARWorldMap,NSError>) :

// Local storage
var PersistentWorldPath => Environment.GetFolderPath(Environment.SpecialFolder.Personal) + "/arworldmap";

// Later, after scanning the environment thoroughly...
var worldMap = await Session.GetCurrentWorldMapAsync();
if (worldMap != null)
{
    var data = NSKeyedArchiver.ArchivedDataWithRootObject(worldMap, true, out var err);
    if (err != null)
    {
        Console.WriteLine(err);
    }
    File.WriteAllBytes(PersistentWorldPath, data.ToArray());
}

Чтобы предоставить общий доступ к карте мира или восстановить ее, выполните следующие действия:

  1. Загрузка данных из файла,
  2. Разархивив его в ARWorldMap объект,
  3. Используйте это в качестве значения для ARWorldTrackingConfiguration.InitialWorldMap свойства:
var data = NSData.FromArray(File.ReadAllBytes(PersistentWorldController.PersistenWorldPath));
var worldMap = (ARWorldMap)NSKeyedUnarchiver.GetUnarchivedObject(typeof(ARWorldMap), data, out var err);

var configuration = new ARWorldTrackingConfiguration
{
    PlaneDetection = ARPlaneDetection.Horizontal | ARPlaneDetection.Vertical,
    LightEstimationEnabled = true,
    EnvironmentTexturing = AREnvironmentTexturing.Automatic,
    InitialWorldMap = worldMap
};

Только ARWorldMap содержит невидимые данные отслеживания мира и ARAnchor объекты, они не содержат цифровые ресурсы. Чтобы предоставить общий доступ к геометрии или изображениям, необходимо разработать собственную стратегию, соответствующую вашему варианту использования (возможно, путем хранения и передачи только расположения и ориентации геометрии и применения его к статическим SCNGeometry или, возможно, путем хранения и передачи сериализованных объектов). Преимущество ARWorldMap заключается в том, что ресурсы после размещения относительно общего ARAnchorдоступа будут отображаться последовательно между устройствами или сеансами.

Формат файла описания универсальной сцены

Последней функцией заголовка ARKit 2 является принятие Apple универсального формата описания сцены Pixar. Этот формат заменяет формат DAE Collada в качестве предпочтительного формата для совместного использования и хранения ресурсов ARKit. Поддержка визуализации ресурсов встроена в iOS 12 и Mojave. Расширение ФАЙЛА USDZ — это несжатый и незашифрованный ZIP-архив, содержащий ФАЙЛЫ USD. Pixar предоставляет средства для работы с файлами USD, но пока нет много сторонней поддержки.

Советы по программированию ARKit

Управление ресурсами вручную

В ARKit важно вручную управлять ресурсами. Это не только позволяет высокой частоте кадров, на самом деле необходимо избежать запутанной "заморозки экрана". Платформа ARKit отложена о предоставлении нового кадра камеры (ARSession.CurrentFrame. До тех пор, пока текущий ARFrame Dispose() вызов на него, ARKit не будет предоставлять новый кадр! Это приводит к тому, что видео будет "заморозить", несмотря на то, что остальная часть приложения реагирует. Решение — всегда обращаться ARSession.CurrentFrame к блоку using или вручную вызывать Dispose() его.

Все объекты, производные от NSObject них, реализуют IDisposable NSObject шаблон Dispose, поэтому обычно следует следовать этому шаблону для реализации Dispose в производном классе.

Управление матрицами преобразования

В любом трехмерном приложении вы будете иметь дело с матрицами преобразования 4x4, которые компактно описывают перемещение, поворот и сдвиг объекта через трехмерное пространство. В SceneKit это SCNMatrix4 объекты.

Свойство SCNNode.Transform возвращает матрицу преобразования для SCNNode типа с поддержкой SCNMatrix4 основного simdfloat4x4 типа строки. Например, например:

var node = new SCNNode { Position = new SCNVector3(2, 3, 4) };  
var xform = node.Transform;
Console.WriteLine(xform);
// Output is: "(1, 0, 0, 0)\n(0, 1, 0, 0)\n(0, 0, 1, 0)\n(2, 3, 4, 1)"

Как видно, позиция закодирована в первых трех элементах нижней строки.

В Xamarin общий тип для управления матрицами преобразования является NVector4то, что по соглашению интерпретируется в основном столбце. То есть компонент перевода и позиции ожидается в M14, M24, M34, а не M41, M42, M43:

row-major и column-major

Согласование с выбором интерпретации матрицы жизненно важно для правильного поведения. Так как матрицы преобразования 3D имеют значение 4x4, ошибки согласованности не будут создавать никаких исключений во время компиляции или даже во время выполнения. Это просто то, что операции будут действовать неожиданно. Если объекты SceneKit / ARKit, кажется, застряли, улетают или дрожут, неправильное преобразование матрицы является хорошей возможностью. Решение просто NMatrix4.Transpose : выполняется транспозиция элементов на месте.